Merge branch 'develop' into matthew/status

This commit is contained in:
Matthew Hodgson 2017-11-01 15:24:30 +00:00
commit 64d8c81897
9 changed files with 119 additions and 63 deletions

View file

@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig'; import SdkConfig from './SdkConfig';
function getRedactedUrl() { function getRedactedUrl() {
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>"); const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
// hardcoded url to make piwik happy // hardcoded url to make piwik happy
return 'https://riot.im/app/' + redactedHash; return 'https://riot.im/app/' + redactedHash;
} }

View file

@ -159,6 +159,7 @@ export default class Login {
accessToken: data.access_token, accessToken: data.access_token,
}); });
}).catch((fallback_error) => { }).catch((fallback_error) => {
console.log("fallback HS login failed", fallback_error);
// throw the original error // throw the original error
throw originalError; throw originalError;
}); });
@ -179,6 +180,7 @@ export default class Login {
accessToken: data.access_token, accessToken: data.access_token,
}); });
}).catch((fallback_error) => { }).catch((fallback_error) => {
console.log("Lowercase username login failed", fallback_error);
// throw the original error // throw the original error
throw originalError; throw originalError;
}); });
@ -210,6 +212,9 @@ export default class Login {
return tryLowercaseUsername(originalLoginError); return tryLowercaseUsername(originalLoginError);
} }
throw originalLoginError; throw originalLoginError;
}).catch((error) => {
console.log("Login failed", error);
throw error;
}); });
} }

View file

@ -407,6 +407,10 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
summary: null, summary: null,
isGroupPublicised: null,
isUserPrivileged: null,
groupRooms: null,
groupRoomsLoading: null,
error: null, error: null,
editing: false, editing: false,
saving: false, saving: false,
@ -458,8 +462,14 @@ export default React.createClass({
} }
this.setState({ this.setState({
summary, summary,
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: this._groupStore.getGroupPublicity(), isGroupPublicised: this._groupStore.getGroupPublicity(),
isUserPrivileged: this._groupStore.isUserPrivileged(), isUserPrivileged: this._groupStore.isUserPrivileged(),
groupRooms: this._groupStore.getGroupRooms(),
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
isUserMember: this._groupStore.getGroupMembers().some(
(m) => m.userId === MatrixClientPeg.get().credentials.userId,
),
error: null, error: null,
}); });
}); });
@ -650,6 +660,7 @@ export default React.createClass({
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList'); const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
const Spinner = sdk.getComponent('elements.Spinner');
const addRoomRow = this.state.editing ? const addRoomRow = this.state.editing ?
(<AccessibleButton className="mx_GroupView_rooms_header_addRow" (<AccessibleButton className="mx_GroupView_rooms_header_addRow"
@ -667,7 +678,10 @@ export default React.createClass({
<h3>{ _t('Rooms') }</h3> <h3>{ _t('Rooms') }</h3>
{ addRoomRow } { addRoomRow }
</div> </div>
<RoomDetailList rooms={this._groupStore.getGroupRooms()} /> { this.state.groupRoomsLoading ?
<Spinner /> :
<RoomDetailList rooms={this.state.groupRooms} />
}
</div>; </div>;
}, },
@ -863,7 +877,7 @@ export default React.createClass({
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.state.summary === null && this.state.error === null || this.state.saving) { if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
return <Spinner />; return <Spinner />;
} else if (this.state.summary) { } else if (this.state.summary) {
const summary = this.state.summary; const summary = this.state.summary;
@ -884,6 +898,7 @@ export default React.createClass({
} else { } else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId} avatarImage = <GroupAvatar groupId={this.props.groupId}
groupName={this.state.profileForm.name}
groupAvatarUrl={this.state.profileForm.avatar_url} groupAvatarUrl={this.state.profileForm.avatar_url}
width={48} height={48} resizeMethod='crop' width={48} height={48} resizeMethod='crop'
/>; />;
@ -927,25 +942,28 @@ export default React.createClass({
tabIndex="2" tabIndex="2"
dir="auto" />; dir="auto" />;
} else { } else {
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
const groupName = summary.profile ? summary.profile.name : null;
avatarNode = <GroupAvatar avatarNode = <GroupAvatar
groupId={this.props.groupId} groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl} groupAvatarUrl={groupAvatarUrl}
onClick={this._onEditClick} groupName={groupName}
onClick={onGroupHeaderItemClick}
width={48} height={48} width={48} height={48}
/>; />;
if (summary.profile && summary.profile.name) { if (summary.profile && summary.profile.name) {
nameNode = <div onClick={this._onEditClick}> nameNode = <div onClick={onGroupHeaderItemClick}>
<span>{ summary.profile.name }</span> <span>{ summary.profile.name }</span>
<span className="mx_GroupView_header_groupid"> <span className="mx_GroupView_header_groupid">
({ this.props.groupId }) ({ this.props.groupId })
</span> </span>
</div>; </div>;
} else { } else {
nameNode = <span onClick={this._onEditClick}>{ this.props.groupId }</span>; nameNode = <span onClick={onGroupHeaderItemClick}>{ this.props.groupId }</span>;
} }
if (summary.profile && summary.profile.short_description) { if (summary.profile && summary.profile.short_description) {
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>; shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
} }
} }
if (this.state.editing) { if (this.state.editing) {
@ -986,6 +1004,7 @@ export default React.createClass({
const headerClasses = { const headerClasses = {
mx_GroupView_header: true, mx_GroupView_header: true,
mx_GroupView_header_view: !this.state.editing, mx_GroupView_header_view: !this.state.editing,
mx_GroupView_header_isUserMember: this.state.isUserMember,
}; };
return ( return (

View file

@ -24,6 +24,7 @@ export default React.createClass({
propTypes: { propTypes: {
groupId: PropTypes.string, groupId: PropTypes.string,
groupName: PropTypes.string,
groupAvatarUrl: PropTypes.string, groupAvatarUrl: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
@ -57,7 +58,7 @@ export default React.createClass({
return ( return (
<BaseAvatar <BaseAvatar
name={this.props.groupId[1]} name={this.props.groupName || this.props.groupId[1]}
idName={this.props.groupId} idName={this.props.groupId}
url={this.getGroupAvatarUrl()} url={this.getGroupAvatarUrl()}
{...otherProps} {...otherProps}

View file

@ -55,8 +55,8 @@ export default React.createClass({
_checkGroupId: function(e) { _checkGroupId: function(e) {
let error = null; let error = null;
if (!/^[a-zA-Z0-9]*$/.test(this.state.groupId)) { if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain alphanumeric characters"); error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
} }
this.setState({ this.setState({
groupIdError: error, groupIdError: error,

View file

@ -17,50 +17,66 @@ limitations under the License.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import { groupMemberFromApiObject } from '../../../groups'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'GroupMemberInfo', displayName: 'GroupMemberInfo',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
propTypes: { propTypes: {
matrixClient: PropTypes.object.isRequired,
groupId: PropTypes.string, groupId: PropTypes.string,
groupMember: GroupMemberType, groupMember: GroupMemberType,
isInvited: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
fetching: false,
removingUser: false, removingUser: false,
groupMembers: null, isUserPrivilegedInGroup: null,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
this._fetchMembers(); this._initGroupStore(this.props.groupId);
}, },
_fetchMembers: function() { componentWillReceiveProps(newProps) {
this.setState({fetching: true}); if (newProps.groupId !== this.props.groupId) {
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => { this._unregisterGroupStore();
this._initGroupStore(newProps.groupId);
}
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(
this.context.matrixClient, this.props.groupId,
);
this._groupStore.registerListener(this.onGroupStoreUpdated);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
},
onGroupStoreUpdated: function() {
this.setState({ this.setState({
groupMembers: result.chunk.map((apiMember) => { isUserInvited: this._groupStore.getGroupInvitedMembers().some(
return groupMemberFromApiObject(apiMember); (m) => m.userId === this.props.groupMember.userId,
}), ),
fetching: false, isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group groupMember list: ", e);
}); });
}, },
@ -68,13 +84,13 @@ module.exports = withMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, { Modal.createDialog(ConfirmUserActionDialog, {
groupMember: this.props.groupMember, groupMember: this.props.groupMember,
action: _t('Remove from community'), action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
danger: true, danger: true,
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return; if (!proceed) return;
this.setState({removingUser: true}); this.setState({removingUser: true});
this.props.matrixClient.removeUserFromGroup( this.context.matrixClient.removeUserFromGroup(
this.props.groupId, this.props.groupMember.userId, this.props.groupId, this.props.groupMember.userId,
).then(() => { ).then(() => {
// return to the user list // return to the user list
@ -86,7 +102,9 @@ module.exports = withMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
description: _t('Failed to remove user from community'), description: this.state.isUserInvited ?
_t('Failed to withdraw invitation') :
_t('Failed to remove user from community'),
}); });
}).finally(() => { }).finally(() => {
this.setState({removingUser: false}); this.setState({removingUser: false});
@ -111,24 +129,17 @@ module.exports = withMatrixClient(React.createClass({
}, },
render: function() { render: function() {
if (this.state.fetching || this.state.removingUser) { if (this.state.removingUser) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />; return <Spinner />;
} }
if (!this.state.groupMembers) return null;
const targetIsInGroup = this.state.groupMembers.some((m) => { let adminTools;
return m.userId === this.props.groupMember.userId; if (this.state.isUserPrivilegedInGroup) {
}); const kickButton = (
let kickButton;
let adminButton;
if (targetIsInGroup) {
kickButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
onClick={this._onKick}> onClick={this._onKick}>
{ _t('Remove from community') } { this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton> </AccessibleButton>
); );
@ -137,22 +148,19 @@ module.exports = withMatrixClient(React.createClass({
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}> giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</AccessibleButton>;*/ </AccessibleButton>;*/
}
let adminTools; if (kickButton) {
if (kickButton || adminButton) {
adminTools = adminTools =
<div className="mx_MemberInfo_adminTools"> <div className="mx_MemberInfo_adminTools">
<h3>{ _t("Admin Tools") }</h3> <h3>{ _t("Admin Tools") }</h3>
<div className="mx_MemberInfo_buttons"> <div className="mx_MemberInfo_buttons">
{ kickButton } { kickButton }
{ adminButton }
</div> </div>
</div>; </div>;
} }
}
const avatarUrl = this.props.matrixClient.mxcUrlToHttp( const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupMember.avatarUrl, this.props.groupMember.avatarUrl,
36, 36, 'crop', 36, 36, 'crop',
); );
@ -192,4 +200,4 @@ module.exports = withMatrixClient(React.createClass({
</div> </div>
); );
}, },
})); });

View file

@ -184,7 +184,8 @@ module.exports = React.createClass({
}); });
}, },
onClickChange: function() { onClickChange: function(ev) {
ev.preventDefault();
const oldPassword = this.state.cachedPassword || this.refs.old_input.value; const oldPassword = this.state.cachedPassword || this.refs.old_input.value;
const newPassword = this.refs.new_input.value; const newPassword = this.refs.new_input.value;
const confirmPassword = this.refs.confirm_input.value; const confirmPassword = this.refs.confirm_input.value;

View file

@ -152,7 +152,6 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Communities": "Communities", "Communities": "Communities",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Mention": "Mention",
"%(displayName)s is typing": "%(displayName)s is typing", "%(displayName)s is typing": "%(displayName)s is typing",
"%(names)s and one other are typing": "%(names)s and one other are typing", "%(names)s and one other are typing": "%(names)s and one other are typing",
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
@ -240,6 +239,7 @@
"Unignore": "Unignore", "Unignore": "Unignore",
"Ignore": "Ignore", "Ignore": "Ignore",
"Jump to read receipt": "Jump to read receipt", "Jump to read receipt": "Jump to read receipt",
"Mention": "Mention",
"Invite": "Invite", "Invite": "Invite",
"User Options": "User Options", "User Options": "User Options",
"Direct chats": "Direct chats", "Direct chats": "Direct chats",
@ -489,6 +489,7 @@
"Identity server URL": "Identity server URL", "Identity server URL": "Identity server URL",
"What does this mean?": "What does this mean?", "What does this mean?": "What does this mean?",
"Remove from community": "Remove from community", "Remove from community": "Remove from community",
"Failed to withdraw invitation": "Failed to withdraw invitation",
"Failed to remove user from community": "Failed to remove user from community", "Failed to remove user from community": "Failed to remove user from community",
"Filter community members": "Filter community members", "Filter community members": "Filter community members",
"Filter community rooms": "Filter community rooms", "Filter community rooms": "Filter community rooms",
@ -589,7 +590,7 @@
"Confirm Removal": "Confirm Removal", "Confirm Removal": "Confirm Removal",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
"%(actionVerb)s this person?": "%(actionVerb)s this person?", "%(actionVerb)s this person?": "%(actionVerb)s this person?",
"Community IDs may only contain alphanumeric characters": "Community IDs may only contain alphanumeric characters", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
"Something went wrong whilst creating your community": "Something went wrong whilst creating your community", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
"Create Community": "Create Community", "Create Community": "Create Community",
"Community Name": "Community Name", "Community Name": "Community Name",

View file

@ -23,12 +23,23 @@ import FlairStore from './FlairStore';
* other useful group APIs that may have an effect on the group summary. * other useful group APIs that may have an effect on the group summary.
*/ */
export default class GroupStore extends EventEmitter { export default class GroupStore extends EventEmitter {
static STATE_KEY = {
GroupMembers: 'GroupMembers',
GroupInvitedMembers: 'GroupInvitedMembers',
Summary: 'Summary',
GroupRooms: 'GroupRooms',
};
constructor(matrixClient, groupId) { constructor(matrixClient, groupId) {
super(); super();
this.groupId = groupId; this.groupId = groupId;
this._matrixClient = matrixClient; this._matrixClient = matrixClient;
this._summary = {}; this._summary = {};
this._rooms = []; this._rooms = [];
this._members = [];
this._invitedMembers = [];
this._ready = {};
this.on('error', (err) => { this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err); console.error(`GroupStore for ${this.groupId} encountered error`, err);
@ -40,6 +51,7 @@ export default class GroupStore extends EventEmitter {
this._members = result.chunk.map((apiMember) => { this._members = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember); return groupMemberFromApiObject(apiMember);
}); });
this._ready[GroupStore.STATE_KEY.GroupMembers] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
console.error("Failed to get group member list: " + err); console.error("Failed to get group member list: " + err);
@ -50,6 +62,7 @@ export default class GroupStore extends EventEmitter {
this._invitedMembers = result.chunk.map((apiMember) => { this._invitedMembers = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember); return groupMemberFromApiObject(apiMember);
}); });
this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
// Invited users not visible to non-members // Invited users not visible to non-members
@ -64,6 +77,7 @@ export default class GroupStore extends EventEmitter {
_fetchSummary() { _fetchSummary() {
this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._matrixClient.getGroupSummary(this.groupId).then((resp) => {
this._summary = resp; this._summary = resp;
this._ready[GroupStore.STATE_KEY.Summary] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
this.emit('error', err); this.emit('error', err);
@ -75,6 +89,7 @@ export default class GroupStore extends EventEmitter {
this._rooms = resp.chunk.map((apiRoom) => { this._rooms = resp.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom); return groupRoomFromApiObject(apiRoom);
}); });
this._ready[GroupStore.STATE_KEY.GroupRooms] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
this.emit('error', err); this.emit('error', err);
@ -87,6 +102,8 @@ export default class GroupStore extends EventEmitter {
registerListener(fn) { registerListener(fn) {
this.on('update', fn); this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
this._fetchSummary(); this._fetchSummary();
this._fetchRooms(); this._fetchRooms();
this._fetchMembers(); this._fetchMembers();
@ -96,6 +113,10 @@ export default class GroupStore extends EventEmitter {
this.removeListener('update', fn); this.removeListener('update', fn);
} }
isStateReady(id) {
return this._ready[id];
}
getSummary() { getSummary() {
return this._summary; return this._summary;
} }