From 5209f29a5cc7b7ed6b3bbb661f895eed9cfa1680 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 18:27:54 +0100 Subject: [PATCH 01/17] Use "crop" method to scale group avatars in MyGroups --- src/components/structures/MyGroups.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index bb0d42435e..b6a450fbb4 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -62,7 +62,9 @@ const GroupTile = React.createClass({ const profile = this.state.profile || {}; const name = profile.name || this.props.groupId; const desc = profile.shortDescription; - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(profile.avatarUrl, 50, 50) : null; + const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + profile.avatarUrl, 50, 50, "crop", + ) : null; return
From 5312a869e4d6a48564c3cf50adf2e7b6f044a1c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Oct 2017 18:36:59 +0100 Subject: [PATCH 02/17] Try lowercase username on login Fixes https://github.com/vector-im/riot-web/issues/5446 --- src/Login.js | 74 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Login.js b/src/Login.js index 0eff94ce60..f1fb9a5b61 100644 --- a/src/Login.js +++ b/src/Login.js @@ -143,6 +143,48 @@ export default class Login { Object.assign(loginParams, legacyParams); const client = this._createTemporaryClient(); + + const tryFallbackHs = (originalError) => { + const fbClient = Matrix.createClient({ + baseUrl: self._fallbackHsUrl, + idBaseUrl: this._isUrl, + }); + + return fbClient.login('m.login.password', loginParams).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._fallbackHsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }, function(fallback_error) { + // throw the original error + throw originalError; + }); + }; + const tryLowercaseUsername = (originalError) => { + const loginParamsLowercase = Object.assign({}, loginParams, { + user: username.toLowerCase(), + identifier: { + user: username.toLowerCase(), + }, + }); + return client.login('m.login.password', loginParamsLowercase).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._fallbackHsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }, function(fallback_error) { + // throw the original error + throw originalError; + }); + }; + + let originalLoginError = null; return client.login('m.login.password', loginParams).then(function(data) { return Promise.resolve({ homeserverUrl: self._hsUrl, @@ -151,29 +193,23 @@ export default class Login { deviceId: data.device_id, accessToken: data.access_token, }); - }, function(error) { + }).catch((error) => { + originalLoginError = error; if (error.httpStatus === 403) { if (self._fallbackHsUrl) { - const fbClient = Matrix.createClient({ - baseUrl: self._fallbackHsUrl, - idBaseUrl: this._isUrl, - }); - - return fbClient.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._fallbackHsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }, function(fallback_error) { - // throw the original error - throw error; - }); + return tryFallbackHs(originalLoginError); } } - throw error; + throw originalLoginError; + }).catch((error) => { + if ( + error.httpStatus === 403 && + loginParams.identifier.type === 'm.id.user' && + username.search(/[A-Z]/) > -1 + ) { + return tryLowercaseUsername(originalLoginError); + } + throw originalLoginError; }); } From b437a2559dd62207272bda52221c46e7c4543230 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Oct 2017 18:59:13 +0100 Subject: [PATCH 03/17] PR feedback --- src/Login.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Login.js b/src/Login.js index f1fb9a5b61..9039c9f511 100644 --- a/src/Login.js +++ b/src/Login.js @@ -158,7 +158,7 @@ export default class Login { deviceId: data.device_id, accessToken: data.access_token, }); - }, function(fallback_error) { + }).catch((fallback_error) => { // throw the original error throw originalError; }); @@ -172,13 +172,13 @@ export default class Login { }); return client.login('m.login.password', loginParamsLowercase).then(function(data) { return Promise.resolve({ - homeserverUrl: self._fallbackHsUrl, + homeserverUrl: self._hsUrl, identityServerUrl: self._isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, }); - }, function(fallback_error) { + }).catch((fallback_error) => { // throw the original error throw originalError; }); From 14d600a69f3e91734edc2e7d9ab47c3085d9f7e0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Oct 2017 15:04:12 +0000 Subject: [PATCH 04/17] Fix initial in GroupAvatar in GroupView --- src/components/structures/GroupView.js | 3 +++ src/components/views/avatars/GroupAvatar.js | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1564efd8d3..fa75df53f0 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -884,6 +884,7 @@ export default React.createClass({ } else { const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); avatarImage = ; @@ -928,9 +929,11 @@ export default React.createClass({ dir="auto" />; } else { const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; + const groupName = summary.profile ? summary.profile.name : null; avatarNode = ; 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 ( Date: Mon, 30 Oct 2017 16:28:27 +0000 Subject: [PATCH 05/17] Don't refresh page on password change prompt It's on the form submit but missing a preventDefault --- src/components/views/settings/ChangePassword.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; From 3e64333adab5772af8e15263142940a0b0a3e760 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Oct 2017 16:45:45 +0000 Subject: [PATCH 06/17] Only show admin tools to privileged users --- .../views/groups/GroupMemberInfo.js | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index ff100c418a..5bae4d65d2 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -17,50 +17,62 @@ limitations under the License. import PropTypes from 'prop-types'; import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; -import { groupMemberFromApiObject } from '../../../groups'; -import withMatrixClient from '../../../wrappers/withMatrixClient'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; import AccessibleButton from '../elements/AccessibleButton'; import GeminiScrollbar from 'react-gemini-scrollbar'; - -module.exports = withMatrixClient(React.createClass({ +module.exports = React.createClass({ displayName: 'GroupMemberInfo', + contextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), + }, + propTypes: { - matrixClient: PropTypes.object.isRequired, groupId: PropTypes.string, groupMember: GroupMemberType, }, getInitialState: function() { return { - fetching: false, removingUser: false, - groupMembers: null, + isUserPrivilegedInGroup: null, }; }, componentWillMount: function() { - this._fetchMembers(); + this._initGroupStore(this.props.groupId); }, - _fetchMembers: function() { - this.setState({fetching: true}); - this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => { - 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({ + isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(), }); }, @@ -74,7 +86,7 @@ module.exports = withMatrixClient(React.createClass({ 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 @@ -111,21 +123,14 @@ 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') } @@ -137,22 +142,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 +194,4 @@ module.exports = withMatrixClient(React.createClass({
); }, -})); +}); From 6874f313e335ce333e6499e141f30089f75d720b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Oct 2017 17:04:21 +0000 Subject: [PATCH 07/17] log login errors --- src/Login.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Login.js b/src/Login.js index 9039c9f511..bc28d5ce8a 100644 --- a/src/Login.js +++ b/src/Login.js @@ -210,6 +210,9 @@ export default class Login { return tryLowercaseUsername(originalLoginError); } throw originalLoginError; + }).catch((error) => { + console.log("Login failed", error); + throw error; }); } From 5ea19e27519f29ee6a9e095883f793b4e5633e01 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Oct 2017 17:15:27 +0000 Subject: [PATCH 08/17] Log errors from other login attempts --- src/Login.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Login.js b/src/Login.js index bc28d5ce8a..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; }); From 4e234cfc3b28cbf986da4714a57d7d013b6a6f6f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Oct 2017 18:17:35 +0000 Subject: [PATCH 09/17] Alter UI for disinviting a group member The same API as kicking is used for disinviting, so only cosmetic changes needed here. --- src/components/views/groups/GroupMemberInfo.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 5bae4d65d2..4c0b54e891 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -37,6 +37,7 @@ module.exports = React.createClass({ propTypes: { groupId: PropTypes.string, groupMember: GroupMemberType, + isInvited: PropTypes.bool, }, getInitialState: function() { @@ -72,6 +73,9 @@ module.exports = React.createClass({ onGroupStoreUpdated: function() { this.setState({ + isUserInvited: this._groupStore.getGroupInvitedMembers().some( + (m) => m.userId === this.props.groupMember.userId, + ), isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(), }); }, @@ -80,7 +84,7 @@ module.exports = 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; @@ -98,7 +102,9 @@ module.exports = 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}); @@ -133,7 +139,7 @@ module.exports = React.createClass({ const kickButton = ( - { _t('Remove from community') } + { this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') } ); From 53938f7998e8c1f0e65de4e06e112ce2d8bada42 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 10:25:06 +0000 Subject: [PATCH 10/17] Change client-side validation of group IDs to match synapse --- src/components/views/dialogs/CreateGroupDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index bda298ef0b..f38d92b482 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -55,8 +55,8 @@ export default React.createClass({ _checkGroupId: function(e) { let error = null; - if (!/^[a-zA-Z0-9]*$/.test(this.state.groupId)) { - error = _t("Community IDs may only contain alphanumeric characters"); + if (!/^[a-z0-9=_-\.\/]*$/.test(this.state.groupId)) { + error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'"); } this.setState({ groupIdError: error, From f53a12d904330cc11deec370434dcd6f8daa9f5a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 10:25:48 +0000 Subject: [PATCH 11/17] Generate en_EN translations --- src/i18n/strings/en_EN.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a541e9e130..bffe3b3264 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", @@ -488,6 +488,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", @@ -588,7 +589,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", From 20bf69c3c24a9785badecaeb8339e7e6d09973c0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 10:54:44 +0000 Subject: [PATCH 12/17] Prevent non-members from opening group settings --- src/components/structures/GroupView.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index fa75df53f0..e643e63df2 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -460,6 +460,9 @@ export default React.createClass({ summary, isGroupPublicised: this._groupStore.getGroupPublicity(), isUserPrivileged: this._groupStore.isUserPrivileged(), + isUserMember: this._groupStore.getGroupMembers().some( + (m) => m.userId === MatrixClientPeg.get().credentials.userId, + ), error: null, }); }); @@ -928,27 +931,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) { @@ -989,6 +993,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 ( From 775468e71a89288ac76fd657413f1b10514cb907 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 11:42:09 +0000 Subject: [PATCH 13/17] Display whether the group summary/room list is loading This uses a `ready` flag assigned to each fetching API used by the GroupServer. I've avoided making this generic for now for want of not doing so early. --- src/components/structures/GroupView.js | 11 +++++++++-- src/components/views/rooms/RoomDetailList.js | 6 ++++++ src/stores/GroupStore.js | 13 +++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index fa75df53f0..76f6bc7335 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,11 @@ export default React.createClass({ } this.setState({ summary, + summaryLoading: !this._groupStore.isStateReady('GroupStore.Summary'), isGroupPublicised: this._groupStore.getGroupPublicity(), isUserPrivileged: this._groupStore.isUserPrivileged(), + groupRooms: this._groupStore.getGroupRooms(), + groupRoomsLoading: !this._groupStore.isStateReady('GroupStore.GroupRooms'), error: null, }); }); @@ -667,7 +674,7 @@ export default React.createClass({

{ _t('Rooms') }

{ addRoomRow }
- + ; }, @@ -863,7 +870,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; diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index be9de849e9..cdd05d1698 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -113,6 +113,8 @@ export default React.createClass({ worldReadable: PropTypes.bool, guestCanJoin: PropTypes.bool, + + loading: PropTypes.bool, })), }, @@ -124,6 +126,10 @@ export default React.createClass({ }, render() { + const Spinner = sdk.getComponent('elements.Spinner'); + if (this.props.loading) { + return ; + } const rows = this.getRows(); let rooms; if (rows.length == 0) { diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 66bc293b44..d3c514f489 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -29,6 +29,9 @@ export default class GroupStore extends EventEmitter { 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 +43,7 @@ export default class GroupStore extends EventEmitter { this._members = result.chunk.map((apiMember) => { return groupMemberFromApiObject(apiMember); }); + this._ready['GroupStore.GroupMembers'] = true; this._notifyListeners(); }).catch((err) => { console.error("Failed to get group member list: " + err); @@ -50,6 +54,7 @@ export default class GroupStore extends EventEmitter { this._invitedMembers = result.chunk.map((apiMember) => { return groupMemberFromApiObject(apiMember); }); + this._ready['GroupStore.GroupInvitedMembers'] = true; this._notifyListeners(); }).catch((err) => { // Invited users not visible to non-members @@ -64,6 +69,7 @@ export default class GroupStore extends EventEmitter { _fetchSummary() { this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._summary = resp; + this._ready['GroupStore.Summary'] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); @@ -75,6 +81,7 @@ export default class GroupStore extends EventEmitter { this._rooms = resp.chunk.map((apiRoom) => { return groupRoomFromApiObject(apiRoom); }); + this._ready['GroupStore.GroupRooms'] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); @@ -87,6 +94,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 +105,10 @@ export default class GroupStore extends EventEmitter { this.removeListener('update', fn); } + isStateReady(id) { + return this._ready[id]; + } + getSummary() { return this._summary; } From 302bd6c3e97862f1dddaf68b9a51e510c11a2510 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 11:48:56 +0000 Subject: [PATCH 14/17] Escape dash in regex --- src/components/views/dialogs/CreateGroupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index f38d92b482..12f419ddd6 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -55,7 +55,7 @@ export default React.createClass({ _checkGroupId: function(e) { let error = null; - if (!/^[a-z0-9=_-\.\/]*$/.test(this.state.groupId)) { + if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) { error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'"); } this.setState({ From d6cbc44e0f4c574c037c599765a3e260254137dc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 14:21:00 +0000 Subject: [PATCH 15/17] If groupRoomsLoading, replace RoomDetailList entirely with Spinner --- src/components/structures/GroupView.js | 6 +++++- src/components/views/rooms/RoomDetailList.js | 6 ------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 76f6bc7335..83e0ad8184 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -657,6 +657,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 ? + : + + } ; }, diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index cdd05d1698..be9de849e9 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -113,8 +113,6 @@ export default React.createClass({ worldReadable: PropTypes.bool, guestCanJoin: PropTypes.bool, - - loading: PropTypes.bool, })), }, @@ -126,10 +124,6 @@ export default React.createClass({ }, render() { - const Spinner = sdk.getComponent('elements.Spinner'); - if (this.props.loading) { - return ; - } const rows = this.getRows(); let rooms; if (rows.length == 0) { From 16dca08b77be54130da8557063b169d9855ee007 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Oct 2017 16:13:13 +0000 Subject: [PATCH 16/17] Use constants instead of string literals --- src/components/structures/GroupView.js | 4 ++-- src/stores/GroupStore.js | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1835325ded..24aa552890 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -462,11 +462,11 @@ export default React.createClass({ } this.setState({ summary, - summaryLoading: !this._groupStore.isStateReady('GroupStore.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.GroupRooms'), + groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms), isUserMember: this._groupStore.getGroupMembers().some( (m) => m.userId === MatrixClientPeg.get().credentials.userId, ), diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index d3c514f489..3afac3c049 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -23,6 +23,14 @@ 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; @@ -43,7 +51,7 @@ export default class GroupStore extends EventEmitter { this._members = result.chunk.map((apiMember) => { return groupMemberFromApiObject(apiMember); }); - this._ready['GroupStore.GroupMembers'] = true; + this._ready[GroupStore.STATE_KEY.GroupMembers] = true; this._notifyListeners(); }).catch((err) => { console.error("Failed to get group member list: " + err); @@ -54,7 +62,7 @@ export default class GroupStore extends EventEmitter { this._invitedMembers = result.chunk.map((apiMember) => { return groupMemberFromApiObject(apiMember); }); - this._ready['GroupStore.GroupInvitedMembers'] = true; + this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true; this._notifyListeners(); }).catch((err) => { // Invited users not visible to non-members @@ -69,7 +77,7 @@ export default class GroupStore extends EventEmitter { _fetchSummary() { this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._summary = resp; - this._ready['GroupStore.Summary'] = true; + this._ready[GroupStore.STATE_KEY.Summary] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); @@ -81,7 +89,7 @@ export default class GroupStore extends EventEmitter { this._rooms = resp.chunk.map((apiRoom) => { return groupRoomFromApiObject(apiRoom); }); - this._ready['GroupStore.GroupRooms'] = true; + this._ready[GroupStore.STATE_KEY.GroupRooms] = true; this._notifyListeners(); }).catch((err) => { this.emit('error', err); From 047bf6e4dd641e2ee350b2c5a0c11c89b6c65201 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 1 Nov 2017 11:30:25 +0000 Subject: [PATCH 17/17] Redact group IDs from analytics --- src/Analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; }