diff --git a/src/Analytics.js b/src/Analytics.js index a82f57a144..1b4f45bc6b 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; function getRedactedUrl() { - const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); + const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/"); // hardcoded url to make piwik happy return 'https://riot.im/app/' + redactedHash; } diff --git a/src/Login.js b/src/Login.js index 9039c9f511..55e996ce80 100644 --- a/src/Login.js +++ b/src/Login.js @@ -159,6 +159,7 @@ export default class Login { accessToken: data.access_token, }); }).catch((fallback_error) => { + console.log("fallback HS login failed", fallback_error); // throw the original error throw originalError; }); @@ -179,6 +180,7 @@ export default class Login { accessToken: data.access_token, }); }).catch((fallback_error) => { + console.log("Lowercase username login failed", fallback_error); // throw the original error throw originalError; }); @@ -210,6 +212,9 @@ export default class Login { return tryLowercaseUsername(originalLoginError); } throw originalLoginError; + }).catch((error) => { + console.log("Login failed", error); + throw error; }); } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1564efd8d3..24aa552890 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -407,6 +407,10 @@ export default React.createClass({ getInitialState: function() { return { summary: null, + isGroupPublicised: null, + isUserPrivileged: null, + groupRooms: null, + groupRoomsLoading: null, error: null, editing: false, saving: false, @@ -458,8 +462,14 @@ export default React.createClass({ } this.setState({ summary, + summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary), isGroupPublicised: this._groupStore.getGroupPublicity(), 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, }); }); @@ -650,6 +660,7 @@ export default React.createClass({ const RoomDetailList = sdk.getComponent('rooms.RoomDetailList'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); + const Spinner = sdk.getComponent('elements.Spinner'); const addRoomRow = this.state.editing ? ({ _t('Rooms') } { addRoomRow } - + { this.state.groupRoomsLoading ? + : + + } ; }, @@ -863,7 +877,7 @@ export default React.createClass({ const Spinner = sdk.getComponent("elements.Spinner"); 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 ; } else if (this.state.summary) { const summary = this.state.summary; @@ -884,6 +898,7 @@ export default React.createClass({ } else { const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); avatarImage = ; @@ -927,25 +942,28 @@ export default React.createClass({ tabIndex="2" dir="auto" />; } else { + const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null; const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; + const groupName = summary.profile ? summary.profile.name : null; avatarNode = ; if (summary.profile && summary.profile.name) { - nameNode =
+ nameNode =
{ summary.profile.name } ({ this.props.groupId })
; } else { - nameNode = { this.props.groupId }; + nameNode = { this.props.groupId }; } if (summary.profile && summary.profile.short_description) { - shortDescNode = { summary.profile.short_description }; + shortDescNode = { summary.profile.short_description }; } } if (this.state.editing) { @@ -986,6 +1004,7 @@ export default React.createClass({ const headerClasses = { mx_GroupView_header: true, mx_GroupView_header_view: !this.state.editing, + mx_GroupView_header_isUserMember: this.state.isUserMember, }; return ( diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js index 3b716f02e1..4f34cc2c16 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.js @@ -24,6 +24,7 @@ export default React.createClass({ propTypes: { groupId: PropTypes.string, + groupName: PropTypes.string, groupAvatarUrl: PropTypes.string, width: PropTypes.number, height: PropTypes.number, @@ -57,7 +58,7 @@ export default React.createClass({ return ( { - this.setState({ - groupMembers: result.chunk.map((apiMember) => { - return groupMemberFromApiObject(apiMember); - }), - fetching: false, - }); - }).catch((e) => { - this.setState({fetching: false}); - console.error("Failed to get group groupMember list: ", e); + componentWillReceiveProps(newProps) { + if (newProps.groupId !== this.props.groupId) { + 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({ + isUserInvited: this._groupStore.getGroupInvitedMembers().some( + (m) => m.userId === this.props.groupMember.userId, + ), + isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(), }); }, @@ -68,13 +84,13 @@ module.exports = withMatrixClient(React.createClass({ const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { groupMember: this.props.groupMember, - action: _t('Remove from community'), + action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), danger: true, onFinished: (proceed) => { if (!proceed) return; this.setState({removingUser: true}); - this.props.matrixClient.removeUserFromGroup( + this.context.matrixClient.removeUserFromGroup( this.props.groupId, this.props.groupMember.userId, ).then(() => { // return to the user list @@ -86,7 +102,9 @@ module.exports = withMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { 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(() => { this.setState({removingUser: false}); @@ -111,24 +129,17 @@ module.exports = withMatrixClient(React.createClass({ }, render: function() { - if (this.state.fetching || this.state.removingUser) { + if (this.state.removingUser) { const Spinner = sdk.getComponent("elements.Spinner"); return ; } - if (!this.state.groupMembers) return null; - const targetIsInGroup = this.state.groupMembers.some((m) => { - return m.userId === this.props.groupMember.userId; - }); - - let kickButton; - let adminButton; - - if (targetIsInGroup) { - kickButton = ( + let adminTools; + if (this.state.isUserPrivilegedInGroup) { + const kickButton = ( - { _t('Remove from community') } + { this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') } ); @@ -137,22 +148,19 @@ module.exports = withMatrixClient(React.createClass({ giveModButton = {giveOpLabel} ;*/ + + if (kickButton) { + adminTools = +
+

{ _t("Admin Tools") }

+
+ { kickButton } +
+
; + } } - let adminTools; - if (kickButton || adminButton) { - adminTools = -
-

{ _t("Admin Tools") }

- -
- { kickButton } - { adminButton } -
-
; - } - - const avatarUrl = this.props.matrixClient.mxcUrlToHttp( + const avatarUrl = this.context.matrixClient.mxcUrlToHttp( this.props.groupMember.avatarUrl, 36, 36, 'crop', ); @@ -192,4 +200,4 @@ module.exports = withMatrixClient(React.createClass({
); }, -})); +}); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index cb49048a3b..9d8b51c7e8 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -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 newPassword = this.refs.new_input.value; const confirmPassword = this.refs.confirm_input.value; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9a79227961..9e486ebe72 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -152,7 +152,6 @@ "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "Communities": "Communities", "Message Pinning": "Message Pinning", - "Mention": "Mention", "%(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 %(count)s others are typing|other": "%(names)s and %(count)s others are typing", @@ -240,6 +239,7 @@ "Unignore": "Unignore", "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", + "Mention": "Mention", "Invite": "Invite", "User Options": "User Options", "Direct chats": "Direct chats", @@ -489,6 +489,7 @@ "Identity server URL": "Identity server URL", "What does this mean?": "What does this mean?", "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", "Filter community members": "Filter community members", "Filter community rooms": "Filter community rooms", @@ -589,7 +590,7 @@ "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.", "%(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", "Create Community": "Create Community", "Community Name": "Community Name", diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 66bc293b44..3afac3c049 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -23,12 +23,23 @@ import FlairStore from './FlairStore'; * other useful group APIs that may have an effect on the group summary. */ export default class GroupStore extends EventEmitter { + + static STATE_KEY = { + GroupMembers: 'GroupMembers', + GroupInvitedMembers: 'GroupInvitedMembers', + Summary: 'Summary', + GroupRooms: 'GroupRooms', + }; + constructor(matrixClient, groupId) { super(); this.groupId = groupId; this._matrixClient = matrixClient; this._summary = {}; this._rooms = []; + this._members = []; + this._invitedMembers = []; + this._ready = {}; this.on('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) => { return groupMemberFromApiObject(apiMember); }); + this._ready[GroupStore.STATE_KEY.GroupMembers] = true; this._notifyListeners(); }).catch((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) => { return groupMemberFromApiObject(apiMember); }); + this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true; this._notifyListeners(); }).catch((err) => { // Invited users not visible to non-members @@ -64,6 +77,7 @@ export default class GroupStore extends EventEmitter { _fetchSummary() { this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._summary = resp; + this._ready[GroupStore.STATE_KEY.Summary] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); @@ -75,6 +89,7 @@ export default class GroupStore extends EventEmitter { this._rooms = resp.chunk.map((apiRoom) => { return groupRoomFromApiObject(apiRoom); }); + this._ready[GroupStore.STATE_KEY.GroupRooms] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); @@ -87,6 +102,8 @@ export default class GroupStore extends EventEmitter { registerListener(fn) { this.on('update', fn); + // Call to set initial state (before fetching starts) + this.emit('update'); this._fetchSummary(); this._fetchRooms(); this._fetchMembers(); @@ -96,6 +113,10 @@ export default class GroupStore extends EventEmitter { this.removeListener('update', fn); } + isStateReady(id) { + return this._ready[id]; + } + getSummary() { return this._summary; }