From 1f44233e05d2a9ef1bf00c51004f0d395b9f9fb3 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Thu, 12 Oct 2017 21:24:45 +0200 Subject: [PATCH 01/49] Better translations in RoomList.js Signed-off-by: Stefan Parviainen --- src/components/views/rooms/RoomList.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index da77174dff..56589353f9 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -34,27 +34,18 @@ const Receipt = require('../../../utils/Receipt'); const HIDE_CONFERENCE_CHANS = true; function phraseForSection(section) { - // These would probably be better as individual strings, - // but for some reason we have translations for these strings - // as-is, so keeping it like this for now. - let verb; switch (section) { case 'm.favourite': - verb = _t('to favourite'); - break; + return _t('Drop here to favourite'); case 'im.vector.fake.direct': - verb = _t('to tag direct chat'); - break; + return _t('Drop here to tag direct chat'); case 'im.vector.fake.recent': - verb = _t('to restore'); - break; + return _t('Drop here to restore'); case 'm.lowpriority': - verb = _t('to demote'); - break; + return _t('Drop here to demote'); default: return _t('Drop here to tag %(section)s', {section: section}); } - return _t('Drop here %(toAction)s', {toAction: verb}); } module.exports = React.createClass({ From 9495ccdbb53f8bf7e30388cdb39b61fe6b2dadb2 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Thu, 12 Oct 2017 21:37:12 +0200 Subject: [PATCH 02/49] Don't hardcode ConfirmUserActionDialog title Signed-off-by: Stefan Parviainen --- src/components/views/dialogs/ConfirmUserActionDialog.js | 4 ++-- src/components/views/groups/GroupMemberInfo.js | 1 + src/components/views/rooms/MemberInfo.js | 5 +++-- src/components/views/rooms/RoomSettings.js | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 9091d8975e..64e25df5f1 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -36,6 +36,7 @@ export default React.createClass({ // group member object. Supply either this or 'member' groupMember: GroupMemberType, action: React.PropTypes.string.isRequired, // eg. 'Ban' + title: React.PropTypes.string.isRequired, // eg. 'Ban this user?' // Whether to display a text field for a reason // If true, the second argument to onFinished will @@ -75,7 +76,6 @@ export default React.createClass({ const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action}); const confirmButtonClass = classnames({ 'mx_Dialog_primary': true, 'danger': this.props.danger, @@ -113,7 +113,7 @@ export default React.createClass({ return (
diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 6f1a370f26..aca2b1b222 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -69,6 +69,7 @@ module.exports = withMatrixClient(React.createClass({ Modal.createDialog(ConfirmUserActionDialog, { groupMember: this.props.groupMember, action: _t('Remove from group'), + title: _t('Remove this user from group?'), danger: true, onFinished: (proceed) => { if (!proceed) return; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 856d3ebad4..180db1d5dd 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -247,11 +247,11 @@ module.exports = withMatrixClient(React.createClass({ onKick: function() { const membership = this.props.member.membership; - const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { member: this.props.member, - action: kickLabel, + action: membership === "invite" ? _t("Disinvite") : _t("Kick"), + title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), askReason: membership == "join", danger: true, onFinished: (proceed, reason) => { @@ -285,6 +285,7 @@ module.exports = withMatrixClient(React.createClass({ Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { member: this.props.member, action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"), + title: this.props.member.membership == 'ban' ? _t("Unban this user?") : _t("Ban this user?"), askReason: this.props.member.membership != 'ban', danger: this.props.member.membership != 'ban', onFinished: (proceed, reason) => { diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 9934456597..b1a2f41cec 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -72,6 +72,7 @@ const BannedUser = React.createClass({ Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, { member: this.props.member, action: _t('Unban'), + title: _t('Unban this user?'), danger: false, onFinished: (proceed) => { if (!proceed) return; From 3b91ada4c8806fedada2d3d0f1fd809070044ecc Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Fri, 13 Oct 2017 20:44:01 +0200 Subject: [PATCH 03/49] Departify sending emails and text messages Signed-off-by: Stefan Parviainen --- src/components/structures/login/ForgotPassword.js | 2 +- src/components/views/login/InteractiveAuthEntryComponents.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 3e76291d20..4500e385e5 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -166,7 +166,7 @@ module.exports = React.createClass({ } else if (this.state.progress === "sent_email") { resetPasswordJsx = (
- { _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed the link it contains, click below") }. + { _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 4c53c23f76..d0cd3931e4 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -256,7 +256,7 @@ export const EmailIdentityAuthEntry = React.createClass({ } else { return (
-

{ _t("An email has been sent to") } { this.props.inputs.emailAddress }

+

{ _t("An email has been sent to %(emailAddress)s", { emailAddress: '' + this.props.inputs.emailAddress + '' }) }

{ _t("Please check your email to continue registration.") }

); @@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
-

{ _t("A text message has been sent to") } +{ this._msisdn }

+

{ _t("A text message has been sent to %(msisdn)s", { msisdn: '' + this._msisdn + '' }) }

{ _t("Please enter the code it contains:") }

From a84b42bf24883dc57160f315027cf0802ec7bf49 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Fri, 13 Oct 2017 21:10:50 +0200 Subject: [PATCH 04/49] Departify RoomSettings Signed-off-by: Stefan Parviainen --- src/components/views/rooms/RoomSettings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index b1a2f41cec..0cb12002e7 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -868,21 +868,21 @@ module.exports = React.createClass({ disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)} checked={historyVisibility === "shared"} onChange={this._onHistoryRadioToggle} /> - { _t('Members only') } ({ _t('since the point in time of selecting this option') }) + { _t('Members only since the point in time of selecting this option') })
From 15d1dc1f3b0c441b6feef2b23ccb92325dd538d7 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sun, 15 Oct 2017 16:57:13 +0200 Subject: [PATCH 05/49] Fix indentation Signed-off-by: Stefan Parviainen --- src/components/views/rooms/RoomList.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 56589353f9..e689579650 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -36,13 +36,13 @@ const HIDE_CONFERENCE_CHANS = true; function phraseForSection(section) { switch (section) { case 'm.favourite': - return _t('Drop here to favourite'); + return _t('Drop here to favourite'); case 'im.vector.fake.direct': - return _t('Drop here to tag direct chat'); + return _t('Drop here to tag direct chat'); case 'im.vector.fake.recent': - return _t('Drop here to restore'); + return _t('Drop here to restore'); case 'm.lowpriority': - return _t('Drop here to demote'); + return _t('Drop here to demote'); default: return _t('Drop here to tag %(section)s', {section: section}); } From ad2f54f8ab228383eb602c9cead1083ad16dec61 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sun, 15 Oct 2017 18:01:57 +0200 Subject: [PATCH 06/49] Fix italics and parens Signed-off-by: Stefan Parviainen --- .../views/login/InteractiveAuthEntryComponents.js | 6 +++--- src/components/views/rooms/RoomSettings.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index d0cd3931e4..5f5a74ccd1 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -20,7 +20,7 @@ import url from 'url'; import classnames from 'classnames'; import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import { _t, _tJsx } from '../../../languageHandler'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -256,7 +256,7 @@ export const EmailIdentityAuthEntry = React.createClass({ } else { return (
-

{ _t("An email has been sent to %(emailAddress)s", { emailAddress: '' + this.props.inputs.emailAddress + '' }) }

+

{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => {this.props.inputs.emailAddress}) }

{ _t("Please check your email to continue registration.") }

); @@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
-

{ _t("A text message has been sent to %(msisdn)s", { msisdn: '' + this._msisdn + '' }) }

+

{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => {this._msisdn}) }

{ _t("Please enter the code it contains:") }

diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0cb12002e7..50478eaf43 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -868,21 +868,21 @@ module.exports = React.createClass({ disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)} checked={historyVisibility === "shared"} onChange={this._onHistoryRadioToggle} /> - { _t('Members only since the point in time of selecting this option') }) + { _t('Members only (since the point in time of selecting this option)') }
From 8083dccfa5239340dc8994d743e629fa8b055bd2 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sun, 15 Oct 2017 21:08:41 +0200 Subject: [PATCH 07/49] De-partify SenderProfile Signed-off-by: Stefan Parviainen Also, text does not need to be EmojiText --- .../views/messages/SenderProfile.js | 38 ++++++++++++++----- src/components/views/rooms/EventTile.js | 12 +++--- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 63e3144115..f6940cd4b3 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -19,6 +19,7 @@ import React from 'react'; import sdk from '../../../index'; import Flair from '../elements/Flair.js'; +import { _tJsx } from '../../../languageHandler'; export default function SenderProfile(props) { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -26,27 +27,44 @@ export default function SenderProfile(props) { const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const {msgtype} = mxEvent.getContent(); + // Display sender name by default if nothing else is given + const text = props.text ? props.text : '%(senderName)s'; + if (msgtype === 'm.emote') { return ; // emote message must include the name so don't duplicate it } + // Name + flair + const nameElem = [ + { name || '' }, + props.enableFlair ? + + : null, + ] + + if(props.text) { + // Replace senderName, and wrap surrounding text in spans with the right class + content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [ + p1 ? {p1} : null, + nameElem, + p2 ? {p2} : null, + ]); + } else { + content = nameElem; + } + return (
- { name || '' } - { props.enableFlair ? - - : null - } - { props.aux ? { props.aux } : null } + { content }
); } SenderProfile.propTypes = { mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing - aux: React.PropTypes.string, // stuff to go after the sender name, if anything + text: React.PropTypes.string, // Text to show. Defaults to sender name onClick: React.PropTypes.func, }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 499d0ec09a..812d72a26a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -19,7 +19,7 @@ limitations under the License. const React = require('react'); const classNames = require("classnames"); -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; const Modal = require('../../../Modal'); const sdk = require('../../../index'); @@ -502,12 +502,12 @@ module.exports = withMatrixClient(React.createClass({ } if (needsSenderProfile) { - let aux = null; + let text = null; if (!this.props.tileShape) { - if (msgtype === 'm.image') aux = _t('sent an image'); - else if (msgtype === 'm.video') aux = _t('sent a video'); - else if (msgtype === 'm.file') aux = _t('uploaded a file'); - sender = ; + if (msgtype === 'm.image') text = _td('%(senderName)s sent an image'); + else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video'); + else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); + sender = ; } else { sender = ; } From 468a05c6f1f2e14218cbf7fa083dd9ab9fb73612 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 17 Oct 2017 21:32:35 +0200 Subject: [PATCH 08/49] Fix SenderProfile Signed-off-by: Stefan Parviainen --- src/components/views/messages/SenderProfile.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index f6940cd4b3..afdb97272f 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -27,30 +27,29 @@ export default function SenderProfile(props) { const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const {msgtype} = mxEvent.getContent(); - // Display sender name by default if nothing else is given - const text = props.text ? props.text : '%(senderName)s'; - if (msgtype === 'm.emote') { return ; // emote message must include the name so don't duplicate it } // Name + flair const nameElem = [ - { name || '' }, + { name || '' }, props.enableFlair ? - : null, - ] + ]; + + let content = ''; if(props.text) { // Replace senderName, and wrap surrounding text in spans with the right class content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [ - p1 ? {p1} : null, + p1 ? { p1 } : null, nameElem, - p2 ? {p2} : null, + p2 ? { p2 } : null, ]); } else { content = nameElem; From fc860c66bc0f1d7cb5b4785a1372bbb422fd47e9 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 17 Oct 2017 22:03:49 +0200 Subject: [PATCH 09/49] De-partify RoomPreviewBar Signed-off-by: Stefan Parviainen --- src/components/views/rooms/RoomPreviewBar.js | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 368d81e606..0c0601a504 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -83,10 +83,8 @@ module.exports = React.createClass({ } }, - _roomNameElement: function(fallback) { - fallback = fallback || _t('a room'); - const name = this.props.room ? this.props.room.name : (this.props.room_alias || ""); - return name ? name : fallback; + _roomNameElement: function() { + return this.props.room ? this.props.room.name : (this.props.room_alias || ""); }, render: function() { @@ -150,7 +148,7 @@ module.exports = React.createClass({
); } else if (kicked || banned) { - const roomName = this._roomNameElement(_t('This room')); + const roomName = this._roomNameElement(); const kickerMember = this.props.room.currentState.getMember( myMember.events.member.getSender(), ); @@ -167,9 +165,17 @@ module.exports = React.createClass({ let actionText; if (kicked) { - actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + if(roomName) { + actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + } else { + actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName}); + } } else if (banned) { - actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + if(roomName) { + actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + } else { + actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName}); + } } // no other options possible due to the kicked || banned check above. joinBlock = ( @@ -203,7 +209,7 @@ module.exports = React.createClass({ joinBlock = (
- { _t('You are trying to access %(roomName)s.', {roomName: name}) } + { name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
{ _tJsx("Click here to join the discussion!", /(.*?)<\/a>/, From 7eeed3e0932841fea286d5a7bdad86cfc412c98e Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 17 Oct 2017 23:46:23 +0200 Subject: [PATCH 10/49] Simplify MemberEventListSummary by using pluralization provided by the i18n library Signed-off-by: Stefan Parviainen --- .../views/elements/MemberEventListSummary.js | 154 +++++------------- 1 file changed, 40 insertions(+), 114 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 596838febe..9b303b4bd9 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -86,7 +86,6 @@ module.exports = React.createClass({ const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this._renderNameList(userNames); - const plural = userNames.length > 1; const splitTransitions = transitions.split(','); @@ -101,7 +100,7 @@ module.exports = React.createClass({ const descs = coalescedTransitions.map((t) => { return this._getDescriptionForTransition( - t.transitionType, plural, t.repeats, + t.transitionType, userNames.length, t.repeats, ); }); @@ -208,148 +207,75 @@ module.exports = React.createClass({ * For a certain transition, t, describe what happened to the users that * underwent the transition. * @param {string} t the transition type. - * @param {boolean} plural whether there were multiple users undergoing the same - * transition. + * @param {integer} userCount number of usernames * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - _getDescriptionForTransition(t, plural, repeats) { + _getDescriptionForTransition(t, userCount, repeats) { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. let res = null; switch(t) { case "joined": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sjoined", { severalUsers: "" }) - : _t("%(oneUser)sjoined", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats }); break; case "left": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sleft", { severalUsers: "" }) - : _t("%(oneUser)sleft", { oneUser: "" }); - } - break; + res = (userCount > 1) + ? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats }); + break; case "joined_and_left": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sjoined and left", { severalUsers: "" }) - : _t("%(oneUser)sjoined and left", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: repeats }); break; case "left_and_joined": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" }) - : _t("%(oneUser)sleft and rejoined", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: repeats }); break; case "invite_reject": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)srejected their invitations", { severalUsers: "" }) - : _t("%(oneUser)srejected their invitation", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats }); break; case "invite_withdrawal": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" }) - : _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats }); break; case "invited": - if (repeats > 1) { - res = (plural) - ? _t("were invited %(repeats)s times", { repeats: repeats }) - : _t("was invited %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were invited") - : _t("was invited"); - } + res = (userCount > 1) + ? _t("were invited %(count)s times", { count: repeats }) + : _t("was invited %(count)s times", { count: repeats }); break; case "banned": - if (repeats > 1) { - res = (plural) - ? _t("were banned %(repeats)s times", { repeats: repeats }) - : _t("was banned %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were banned") - : _t("was banned"); - } + res = (userCount > 1) + ? _t("were banned %(count)s times", { count: repeats }) + : _t("was banned %(count)s times", { count: repeats }); break; case "unbanned": - if (repeats > 1) { - res = (plural) - ? _t("were unbanned %(repeats)s times", { repeats: repeats }) - : _t("was unbanned %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were unbanned") - : _t("was unbanned"); - } + res = (userCount > 1) + ? _t("were unbanned %(count)s times", { count: repeats }) + : _t("was unbanned %(count)s times", { count: repeats }); break; case "kicked": - if (repeats > 1) { - res = (plural) - ? _t("were kicked %(repeats)s times", { repeats: repeats }) - : _t("was kicked %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were kicked") - : _t("was kicked"); - } + res = (userCount > 1) + ? _t("were kicked %(count)s times", { count: repeats }) + : _t("was kicked %(count)s times", { count: repeats }); break; case "changed_name": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)schanged their name", { severalUsers: "" }) - : _t("%(oneUser)schanged their name", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: repeats }); break; case "changed_avatar": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)schanged their avatar", { severalUsers: "" }) - : _t("%(oneUser)schanged their avatar", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats }); break; } From ef30ba889b617a22a46c2173d0d388bc480cd305 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 23 Oct 2017 19:55:40 +0200 Subject: [PATCH 11/49] Make MemberEventListSummary more translatable Signed-off-by: Stefan Parviainen --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 9b303b4bd9..6e75c32e0d 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -106,7 +106,7 @@ module.exports = React.createClass({ const desc = this._renderCommaSeparatedList(descs); - return nameList + " " + desc; + return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc }); }); if (!summaries) { From 6406fc3865ff3fe434b5e7c341bcf7a5792e63e0 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 24 Oct 2017 18:32:50 +0200 Subject: [PATCH 12/49] Use plurals in WhoIsTyping --- src/WhoIsTyping.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 6bea2cbb92..0edad8d4a5 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -68,10 +68,8 @@ module.exports = { const names = whoIsTyping.map(function(m) { return m.name; }); - if (othersCount==1) { - return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')}); - } else if (othersCount>1) { - return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); + if (othersCount>=1) { + return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); From b5024cca750d546eb917ff48f41abb1e613d943a Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 24 Oct 2017 19:34:08 +0200 Subject: [PATCH 13/49] Further simplify MemberEventListSummary a bit Signed-off-by: Stefan Parviainen --- src/components/views/elements/MemberEventListSummary.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 6e75c32e0d..6a1566c961 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -302,11 +302,9 @@ module.exports = React.createClass({ return ""; } else if (items.length === 1) { return items[0]; - } else if (remaining) { + } else if (remaining >= 0) { items = items.slice(0, itemLimit); - return (remaining > 1) - ? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } ) - : _t("%(items)s and one other", { items: items.join(', ') }); + return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ) } else { const lastItem = items.pop(); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); From bc034f3083796384f42999bc355534052f149d63 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 24 Oct 2017 19:34:32 +0200 Subject: [PATCH 14/49] Update strings Signed-off-by: Stefan Parviainen --- src/i18n/strings/en_EN.json | 145 +++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 70 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 492113989b..6955ed053a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -153,8 +153,8 @@ "Communities": "Communities", "Message Pinning": "Message Pinning", "%(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", + "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "Failure to create room": "Failure to create room", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", @@ -210,9 +210,9 @@ " (unsupported)": " (unsupported)", "Join as voice or video.": "Join as voice or video.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", - "sent an image": "sent an image", - "sent a video": "sent a video", - "uploaded a file": "uploaded a file", + "%(senderName)s sent an image": "%(senderName)s sent an image", + "%(senderName)s sent a video": "%(senderName)s sent a video", + "%(senderName)s uploaded a file": "%(senderName)s uploaded a file", "Options": "Options", "Undecryptable": "Undecryptable", "Encrypted by a verified device": "Encrypted by a verified device", @@ -225,9 +225,13 @@ "device id: ": "device id: ", "Disinvite": "Disinvite", "Kick": "Kick", + "Disinvite this user?": "Disinvite this user?", + "Kick this user?": "Kick this user?", "Failed to kick": "Failed to kick", "Unban": "Unban", "Ban": "Ban", + "Unban this user?": "Unban this user?", + "Ban this user?": "Ban this user?", "Failed to ban user": "Failed to ban user", "Failed to mute user": "Failed to mute user", "Failed to toggle moderator status": "Failed to toggle moderator status", @@ -312,12 +316,11 @@ "Forget room": "Forget room", "Search": "Search", "Show panel": "Show panel", - "to favourite": "to favourite", - "to tag direct chat": "to tag direct chat", - "to restore": "to restore", - "to demote": "to demote", + "Drop here to favourite": "Drop here to favourite", + "Drop here to tag direct chat": "Drop here to tag direct chat", + "Drop here to restore": "Drop here to restore", + "Drop here to demote": "Drop here to demote", "Drop here to tag %(section)s": "Drop here to tag %(section)s", - "Drop here %(toAction)s": "Drop here %(toAction)s", "Press to start a chat with someone": "Press to start a chat with someone", "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", "Invites": "Invites", @@ -327,20 +330,22 @@ "Low priority": "Low priority", "Historical": "Historical", "Unnamed Room": "Unnamed Room", - "a room": "a room", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", "You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s", "Would you like to accept or decline this invitation?": "Would you like to accept or decline this invitation?", - "This room": "This room", "Reason: %(reasonText)s": "Reason: %(reasonText)s", "Rejoin": "Rejoin", "You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.", + "You have been kicked from this room by %(userName)s.": "You have been kicked from this room by %(userName)s.", "You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.", + "You have been banned from this room by %(userName)s.": "You have been banned from this room by %(userName)s.", + "This room": "This room", "%(roomName)s does not exist.": "%(roomName)s does not exist.", "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", "You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.", + "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", @@ -385,10 +390,9 @@ "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "Who can read history?": "Who can read history?", "Anyone": "Anyone", - "Members only": "Members only", - "since the point in time of selecting this option": "since the point in time of selecting this option", - "since they were invited": "since they were invited", - "since they joined": "since they joined", + "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", + "Members only (since they were invited)": "Members only (since they were invited)", + "Members only (since they joined)": "Members only (since they joined)", "Room Colour": "Room Colour", "Permissions": "Permissions", "The default role for new room members is": "The default role for new room members is", @@ -460,10 +464,10 @@ "Dismiss": "Dismiss", "To continue, please enter your password.": "To continue, please enter your password.", "Password:": "Password:", - "An email has been sent to": "An email has been sent to", + "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", "Please check your email to continue registration.": "Please check your email to continue registration.", "Token incorrect": "Token incorrect", - "A text message has been sent to": "A text message has been sent to", + "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:", "Start authentication": "Start authentication", "powered by Matrix": "powered by Matrix", @@ -485,6 +489,7 @@ "Identity server URL": "Identity server URL", "What does this mean?": "What does this mean?", "Remove from community": "Remove from community", + "Remove this user from community?": "Remove this user from community?", "Failed to remove user from community": "Failed to remove user from community", "Filter community members": "Filter community members", "Filter community rooms": "Filter community rooms", @@ -510,56 +515,57 @@ "Integrations Error": "Integrations Error", "Could not connect to the integration server": "Could not connect to the integration server", "Manage Integrations": "Manage Integrations", - "%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times", - "%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times", - "%(severalUsers)sjoined": "%(severalUsers)sjoined", - "%(oneUser)sjoined": "%(oneUser)sjoined", - "%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sleft %(repeats)s times", - "%(oneUser)sleft %(repeats)s times": "%(oneUser)sleft %(repeats)s times", - "%(severalUsers)sleft": "%(severalUsers)sleft", - "%(oneUser)sleft": "%(oneUser)sleft", - "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)sjoined and left %(repeats)s times", - "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)sjoined and left %(repeats)s times", - "%(severalUsers)sjoined and left": "%(severalUsers)sjoined and left", - "%(oneUser)sjoined and left": "%(oneUser)sjoined and left", - "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)sleft and rejoined %(repeats)s times", - "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sleft and rejoined %(repeats)s times", - "%(severalUsers)sleft and rejoined": "%(severalUsers)sleft and rejoined", - "%(oneUser)sleft and rejoined": "%(oneUser)sleft and rejoined", - "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)srejected their invitations %(repeats)s times", - "%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)srejected their invitation %(repeats)s times", - "%(severalUsers)srejected their invitations": "%(severalUsers)srejected their invitations", - "%(oneUser)srejected their invitation": "%(oneUser)srejected their invitation", - "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)shad their invitations withdrawn %(repeats)s times", - "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)shad their invitation withdrawn %(repeats)s times", - "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)shad their invitations withdrawn", - "%(oneUser)shad their invitation withdrawn": "%(oneUser)shad their invitation withdrawn", - "were invited %(repeats)s times": "were invited %(repeats)s times", - "was invited %(repeats)s times": "was invited %(repeats)s times", - "were invited": "were invited", - "was invited": "was invited", - "were banned %(repeats)s times": "were banned %(repeats)s times", - "was banned %(repeats)s times": "was banned %(repeats)s times", - "were banned": "were banned", - "was banned": "was banned", - "were unbanned %(repeats)s times": "were unbanned %(repeats)s times", - "was unbanned %(repeats)s times": "was unbanned %(repeats)s times", - "were unbanned": "were unbanned", - "was unbanned": "was unbanned", - "were kicked %(repeats)s times": "were kicked %(repeats)s times", - "was kicked %(repeats)s times": "was kicked %(repeats)s times", - "were kicked": "were kicked", - "was kicked": "was kicked", - "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)schanged their name %(repeats)s times", - "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)schanged their name %(repeats)s times", - "%(severalUsers)schanged their name": "%(severalUsers)schanged their name", - "%(oneUser)schanged their name": "%(oneUser)schanged their name", - "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", - "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", - "%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", - "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", - "%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others", - "%(items)s and one other": "%(items)s and one other", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", + "were invited %(count)s times|other": "were invited %(count)s times", + "were invited %(count)s times|one": "were invited", + "was invited %(count)s times|other": "was invited %(count)s times", + "was invited %(count)s times|one": "was invited", + "were banned %(count)s times|other": "were banned %(count)s times", + "were banned %(count)s times|one": "were banned", + "was banned %(count)s times|other": "was banned %(count)s times", + "was banned %(count)s times|one": "was banned", + "were unbanned %(count)s times|other": "were unbanned %(count)s times", + "were unbanned %(count)s times|one": "were unbanned", + "was unbanned %(count)s times|other": "was unbanned %(count)s times", + "was unbanned %(count)s times|one": "was unbanned", + "were kicked %(count)s times|other": "were kicked %(count)s times", + "were kicked %(count)s times|one": "were kicked", + "was kicked %(count)s times|other": "was kicked %(count)s times", + "was kicked %(count)s times|one": "was kicked", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", + "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "Custom level": "Custom level", "Room directory": "Room directory", @@ -581,7 +587,6 @@ "Start Chatting": "Start Chatting", "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", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community", "Create Community": "Create Community", @@ -676,6 +681,7 @@ "Leave %(groupName)s?": "Leave %(groupName)s?", "Leave": "Leave", "Unable to leave room": "Unable to leave room", + "Community Settings": "Community Settings", "Add rooms to this community": "Add rooms to this community", "Featured Rooms:": "Featured Rooms:", "Featured Users:": "Featured Users:", @@ -686,7 +692,6 @@ "Publish this community on your profile": "Publish this community on your profile", "Long Description (HTML)": "Long Description (HTML)", "Description": "Description", - "Community Settings": "Community Settings", "Community %(groupId)s not found": "Community %(groupId)s not found", "This Home server does not support communities": "This Home server does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", @@ -824,7 +829,7 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Once you've followed the link it contains, click below": "Once you've followed the link it contains, click below", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", "I have verified my email address": "I have verified my email address", "Your password has been reset": "Your password has been reset", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device", From 88fd60066fd011d8cb475961e9ae398413974820 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 24 Oct 2017 20:07:57 +0200 Subject: [PATCH 15/49] Fix typo Signed-off-by: Stefan Parviainen --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 6a1566c961..de6f801a21 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -302,7 +302,7 @@ module.exports = React.createClass({ return ""; } else if (items.length === 1) { return items[0]; - } else if (remaining >= 0) { + } else if (remaining > 0) { items = items.slice(0, itemLimit); return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ) } else { From e094c32c62861de392a76417e8cb543dcdd274f9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 11:36:32 +0100 Subject: [PATCH 16/49] Simplify GroupStore listener registration --- src/components/structures/GroupView.js | 3 +-- src/components/views/groups/GroupMemberList.js | 5 +---- src/components/views/groups/GroupRoomList.js | 4 +--- src/stores/GroupStore.js | 14 +++++++++++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 9d87f84aef..1564efd8d3 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -447,7 +447,7 @@ export default React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { const summary = this._groupStore.getSummary(); if (summary.profile) { // Default profile fields should be "" for later sending to the server (which @@ -464,7 +464,6 @@ export default React.createClass({ }); }); this._groupStore.on('error', (err) => { - console.error(err); this.setState({ summary: null, error: err, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index a5ab22eb0e..8658ac19a5 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -50,12 +50,9 @@ export default withMatrixClient(React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { this._fetchMembers(); }); - this._groupStore.on('error', (err) => { - console.error(err); - }); }, _fetchMembers: function() { diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index 3fcfedd486..aeded2dfb0 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -47,16 +47,14 @@ export default React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { this._fetchRooms(); }); this._groupStore.on('error', (err) => { - console.error('Error in group store (listened to by GroupRoomList)', err); this.setState({ rooms: null, }); }); - this._fetchRooms(); }, _fetchRooms: function() { diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 1da1c35a2b..1d3526ecd5 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -29,9 +29,10 @@ export default class GroupStore extends EventEmitter { this._matrixClient = matrixClient; this._summary = {}; this._rooms = []; - this._fetchSummary(); - this._fetchRooms(); - this._fetchMembers(); + + this.on('error', (err) => { + console.error(`GroupStore for ${this.groupId} encountered error`, err); + }); } _fetchMembers() { @@ -80,6 +81,13 @@ export default class GroupStore extends EventEmitter { this.emit('update'); } + registerListener(fn) { + this.on('update', fn); + this._fetchSummary(); + this._fetchRooms(); + this._fetchMembers(); + } + getSummary() { return this._summary; } From 5d0aa8d7f75eabaf2694f00b2fb51a0f1bead43c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 11:37:45 +0100 Subject: [PATCH 17/49] Handle 403 when inspecting invited users as non-member --- src/stores/GroupStore.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 1d3526ecd5..e169e13ddc 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -52,6 +52,10 @@ export default class GroupStore extends EventEmitter { }); this._notifyListeners(); }).catch((err) => { + // Invited users not visible to non-members + if (err.httpStatus === 403) { + return; + } console.error("Failed to get group invited member list: " + err); this.emit('error', err); }); From 175fadbb57a4c8330aff6482f347844258b58a34 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 14:33:10 +0100 Subject: [PATCH 18/49] Add unregiseterListener to GroupStore --- src/stores/GroupStore.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index e169e13ddc..66bc293b44 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -92,6 +92,10 @@ export default class GroupStore extends EventEmitter { this._fetchMembers(); } + unregisterListener(fn) { + this.removeListener('update', fn); + } + getSummary() { return this._summary; } From 5d0b9d73b4f318ef980c001719fd0d990cffe3b0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Oct 2017 16:20:17 +0100 Subject: [PATCH 19/49] Fix prompt to re-use chat room I managed to lose this when refactoring ChatInviteDialog in https://github.com/matrix-org/matrix-react-sdk/pull/1300 Fixes https://github.com/vector-im/riot-web/issues/5119 --- src/RoomInvite.js | 59 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 427f549eb0..42cff3f5d0 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -21,6 +21,8 @@ import Modal from './Modal'; import { getAddressType } from './UserAddress'; import createRoom from './createRoom'; import sdk from './'; +import dis from './dispatcher'; +import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; export function inviteToRoom(roomId, addr) { @@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); if (_isDmChat(addrTexts)) { - // Start a new DM chat - createRoom({dmUserId: addrTexts[0]}).catch((err) => { - console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { - title: _t("Failed to invite user"), - description: ((err && err.message) ? err.message : _t("Operation failed")), + const rooms = _getDirectMessageRooms(addrTexts[0]); + if (rooms.length > 0) { + // A Direct Message room already exists for this user, so select a + // room from a list that is similar to the one in MemberInfo panel + const ChatCreateOrReuseDialog = sdk.getComponent( + "views.dialogs.ChatCreateOrReuseDialog", + ); + const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { + userId: addrTexts[0], + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: addrTexts[0], + }); + close(true); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + close(true); + }, + }).close; + } else { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); }); - }); + } } else { // Start multi user chat let room; @@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) { return addrs; } +function _getDirectMessageRooms(addr) { + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); + const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); + const rooms = []; + dmRooms.forEach(dmRoom => { + let room = MatrixClientPeg.get().getRoom(dmRoom); + if (room) { + const me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (me.membership == 'join') { + rooms.push(room); + } + } + }); + return rooms; +} + From 2f3e0fb04932fc39f42122694416effcf6c15204 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Oct 2017 16:29:56 +0100 Subject: [PATCH 20/49] Lint --- src/RoomInvite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 42cff3f5d0..1979c6d111 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -184,8 +184,8 @@ function _getDirectMessageRooms(addr) { const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); const rooms = []; - dmRooms.forEach(dmRoom => { - let room = MatrixClientPeg.get().getRoom(dmRoom); + dmRooms.forEach((dmRoom) => { + const room = MatrixClientPeg.get().getRoom(dmRoom); if (room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me.membership == 'join') { From 8a64123ab8175a67e4974a8cf13f660b9905eeba Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 16:55:04 +0100 Subject: [PATCH 21/49] Add sensible missing entry generator for MELS tests Fixes vector-im/riot-web#5426 (because we don't test plurals anywhere else) --- test/components/views/elements/MemberEventListSummary-test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 1618fe4cfe..436133c717 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -88,6 +88,9 @@ describe('MemberEventListSummary', function() { sandbox = testUtils.stubClient(); languageHandler.setLanguage('en').done(done); + languageHandler.setMissingEntryGenerator(function(key) { + return key.split('|', 2)[1]; + }); }); afterEach(function() { From 4eb8fe3e6ab23ef6493f0b35f44a12bf973a22e2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Oct 2017 17:49:44 +0100 Subject: [PATCH 22/49] Lowercase all usernames As synapse doesn't accept usernames with capitals in them now Fixes https://github.com/vector-im/riot-web/issues/5445 --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index db488ea237..2ee11f8386 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -302,7 +302,7 @@ module.exports = React.createClass({ } : {}; return this._matrixClient.register( - this.state.formVals.username, + this.state.formVals.username.toLowerCase(), this.state.formVals.password, undefined, // session id: included in the auth dict already auth, From 5209f29a5cc7b7ed6b3bbb661f895eed9cfa1680 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Oct 2017 18:27:54 +0100 Subject: [PATCH 23/49] 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 24/49] 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 25/49] 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 33/49] 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 34/49] 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 35/49] 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 36/49] 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 37/49] 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 38/49] 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 39/49] 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; } From 15bafd6818ed732ef96e66abd879fa730b14aeac Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Wed, 1 Nov 2017 15:55:58 +0100 Subject: [PATCH 40/49] Convert from weblate to counterpart at runtime to make tests happy Signed-off-by: Stefan Parviainen --- src/languageHandler.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index a90b78c40e..da62bfee56 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -252,6 +252,26 @@ function getLangsJson() { }); } +function weblateToCounterpart(inTrs) { + const outTrs = {}; + + for (const key of Object.keys(inTrs)) { + const keyParts = key.split('|', 2); + if (keyParts.length === 2) { + let obj = outTrs[keyParts[0]]; + if (obj === undefined) { + obj = {}; + outTrs[keyParts[0]] = obj; + } + obj[keyParts[1]] = inTrs[key]; + } else { + outTrs[key] = inTrs[key]; + } + } + + return outTrs; +} + function getLanguage(langPath) { return new Promise((resolve, reject) => { request( @@ -261,7 +281,7 @@ function getLanguage(langPath) { reject({err: err, response: response}); return; } - resolve(JSON.parse(body)); + resolve(weblateToCounterpart(JSON.parse(body))); }, ); }); From e1e4fc2dacb7d2979aecd5cf871f5d3f8ca3196f Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Wed, 1 Nov 2017 16:18:48 +0100 Subject: [PATCH 41/49] Make eslint happy Signed-off-by: Stefan Parviainen --- src/components/views/groups/GroupMemberInfo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index a36c32fb07..01270cd79d 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -85,7 +85,8 @@ module.exports = React.createClass({ Modal.createDialog(ConfirmUserActionDialog, { groupMember: this.props.groupMember, action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), - title: this.state.isUserInvited ? _t('Disinvite this user from community?') : _t('Remove this user from community?'), + title: this.state.isUserInvited ? _t('Disinvite this user from community?') + : _t('Remove this user from community?'), danger: true, onFinished: (proceed) => { if (!proceed) return; From 0dcd52d88f9886860eba2d1b39a9a7379070f79b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 1 Nov 2017 17:12:22 +0000 Subject: [PATCH 42/49] Fix some react warnings firing --- src/components/views/avatars/GroupAvatar.js | 4 ++-- src/components/views/rooms/RoomDetailList.js | 24 +++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js index 4f34cc2c16..5a18213eec 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.js @@ -54,11 +54,11 @@ export default React.createClass({ // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ - const {groupId, groupAvatarUrl, ...otherProps} = this.props; + const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props; return ( Date: Wed, 1 Nov 2017 17:13:00 +0000 Subject: [PATCH 43/49] Implement simple GroupRoomInfo which replaces the "X" on the GroupRoomTile with "Remove from community" under Admin Tools. --- src/components/views/groups/GroupRoomInfo.js | 170 +++++++++++++++++++ src/components/views/groups/GroupRoomTile.js | 76 +-------- src/groups.js | 3 + 3 files changed, 178 insertions(+), 71 deletions(-) create mode 100644 src/components/views/groups/GroupRoomInfo.js diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js new file mode 100644 index 0000000000..647651a0d8 --- /dev/null +++ b/src/components/views/groups/GroupRoomInfo.js @@ -0,0 +1,170 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +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 { GroupRoomType } from '../../../groups'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; +import GeminiScrollbar from 'react-gemini-scrollbar'; + +module.exports = React.createClass({ + displayName: 'GroupRoomInfo', + + contextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), + }, + + propTypes: { + groupId: PropTypes.string, + groupRoom: GroupRoomType, + }, + + getInitialState: function() { + return { + removingRoom: false, + isUserPrivilegedInGroup: null, + }; + }, + + componentWillMount: function() { + this._initGroupStore(this.props.groupId); + }, + + 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(), + }); + }, + + _onRemove: function(e) { + const groupId = this.props.groupId; + const roomName = this.props.groupRoom.displayname; + e.preventDefault(); + e.stopPropagation(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, { + title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}), + description: _t("Removing a room from the community will also remove it from the community page."), + button: _t("Remove"), + onFinished: (proceed) => { + if (!proceed) return; + this.setState({removingRoom: true}); + const groupId = this.props.groupId; + const roomId = this.props.groupRoom.roomId; + this._groupStore.removeRoomFromGroup(roomId).then(() => { + dis.dispatch({ + action: "view_group_room_list", + }); + }).catch((err) => { + console.error(`Error whilst removing ${roomId} from ${groupId}`, err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Failed to remove room from community"), + description: _t( + "Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}, + ), + }); + }).finally(() => { + this.setState({removingRoom: false}); + }); + }, + }); + }, + + _onCancel: function(e) { + dis.dispatch({ + action: "view_group_room_list", + }); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const EmojiText = sdk.getComponent('elements.EmojiText'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + if (this.state.removingRoom) { + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + + let adminTools; + if (this.state.isUserPrivilegedInGroup) { + adminTools = +
+

{ _t("Admin Tools") }

+
+ + { _t('Remove from community') } + +
+
; + } + + const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + this.props.groupRoom.avatarUrl, + 36, 36, 'crop', + ); + + const groupRoomName = this.props.groupRoom.displayname; + const avatar = ; + return ( +
+ + + + +
+ { avatar } +
+ + { groupRoomName } + +
+
+ { this.props.groupRoom.canonical_alias } +
+
+ + { adminTools } +
+
+ ); + }, +}); diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 94dc8e593f..e445f06044 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -16,13 +16,10 @@ limitations under the License. import React from 'react'; import {MatrixClient} from 'matrix-js-sdk'; -import { _t } from '../../../languageHandler'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; -import GroupStoreCache from '../../../stores/GroupStoreCache'; -import Modal from '../../../Modal'; const GroupRoomTile = React.createClass({ displayName: 'GroupRoomTile', @@ -32,68 +29,11 @@ const GroupRoomTile = React.createClass({ groupRoom: GroupRoomType.isRequired, }, - getInitialState: function() { - return { - name: this.calculateRoomName(this.props.groupRoom), - }; - }, - - componentWillReceiveProps: function(newProps) { - this.setState({ - name: this.calculateRoomName(newProps.groupRoom), - }); - }, - - calculateRoomName: function(groupRoom) { - return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room"); - }, - - removeRoomFromGroup: function() { - const groupId = this.props.groupId; - const groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - const roomName = this.state.name; - const roomId = this.props.groupRoom.roomId; - groupStore.removeRoomFromGroup(roomId) - .catch((err) => { - console.error(`Error whilst removing ${roomId} from ${groupId}`, err); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { - title: _t("Failed to remove room from community"), - description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}), - }); - }); - }, - onClick: function(e) { - let roomId; - let roomAlias; - if (this.props.groupRoom.canonicalAlias) { - roomAlias = this.props.groupRoom.canonicalAlias; - } else { - roomId = this.props.groupRoom.roomId; - } dis.dispatch({ - action: 'view_room', - room_id: roomId, - room_alias: roomAlias, - }); - }, - - onDeleteClick: function(e) { - const groupId = this.props.groupId; - const roomName = this.state.name; - e.preventDefault(); - e.stopPropagation(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, { - title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}), - description: _t("Removing a room from the community will also remove it from the community page."), - button: _t("Remove"), - onFinished: (success) => { - if (success) { - this.removeRoomFromGroup(); - } - }, + action: 'view_group_room', + groupId: this.props.groupId, + groupRoom: this.props.groupRoom, }); }, @@ -106,7 +46,7 @@ const GroupRoomTile = React.createClass({ ); const av = ( - @@ -118,14 +58,8 @@ const GroupRoomTile = React.createClass({ { av }
- { this.state.name } + { this.props.groupRoom.displayname }
- - - ); }, diff --git a/src/groups.js b/src/groups.js index 06db5d067f..3c80677b0c 100644 --- a/src/groups.js +++ b/src/groups.js @@ -15,6 +15,7 @@ limitations under the License. */ import PropTypes from 'prop-types'; +import { _t } from './languageHandler.js'; export const GroupMemberType = PropTypes.shape({ userId: PropTypes.string.isRequired, @@ -23,6 +24,7 @@ export const GroupMemberType = PropTypes.shape({ }); export const GroupRoomType = PropTypes.shape({ + displayname: PropTypes.string, name: PropTypes.string, roomId: PropTypes.string.isRequired, canonicalAlias: PropTypes.string, @@ -39,6 +41,7 @@ export function groupMemberFromApiObject(apiObject) { export function groupRoomFromApiObject(apiObject) { return { + displayname: apiObject.name || apiObject.canonical_alias || _t("Unnamed Room"), name: apiObject.name, roomId: apiObject.room_id, canonicalAlias: apiObject.canonical_alias, From 80f79e6b8489d3343c03b4787989db598f73f61f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 1 Nov 2017 17:58:45 +0000 Subject: [PATCH 44/49] Generate translations --- src/i18n/strings/en_EN.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bffe3b3264..037f804447 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -158,6 +158,7 @@ "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "Failure to create room": "Failure to create room", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Unnamed Room": "Unnamed Room", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -328,7 +329,6 @@ "Rooms": "Rooms", "Low priority": "Low priority", "Historical": "Historical", - "Unnamed Room": "Unnamed Room", "a room": "a room", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", @@ -491,13 +491,12 @@ "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", - "Failed to remove room from community": "Failed to remove room from community", - "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Remove": "Remove", - "Remove this room from the community": "Remove this room from the community", + "Failed to remove room from community": "Failed to remove room from community", + "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", + "Filter community rooms": "Filter community rooms", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Do you want to load widget from URL:": "Do you want to load widget from URL:", From 4f8d6d8fbed01df0c18518401a7c42243462dd11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Nov 2017 19:42:47 +0000 Subject: [PATCH 45/49] Pillify room notifs in the timeline This scans text nodes in the DOM for room notifications and turns them into pills. Changes the pillification code around a bit so it works with text nodes. Uses the push processor directly to test the event against the room notifiation rule so we know whether this event would actually trigger a room notification (needs to hook into push at a lower level because otherwise our own room notifications would not pillify since our own events never generate notifications). Requires https://github.com/matrix-org/matrix-js-sdk/pull/565 --- src/components/views/elements/Pill.js | 42 ++++++++++-- src/components/views/messages/TextualBody.js | 69 ++++++++++++++++++-- 2 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 51ae85ba5a..a85f83d78c 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -37,11 +37,20 @@ const Pill = React.createClass({ isMessagePillUrl: (url) => { return !!REGEX_LOCAL_MATRIXTO.exec(url); }, + roomNotifPos: (text) => { + return text.indexOf("@room"); + }, + roomNotifLen: () => { + return "@room".length; + }, TYPE_USER_MENTION: 'TYPE_USER_MENTION', TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION', + TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention }, props: { + // The Type of this Pill. If url is given, this is auto-detected. + type: PropTypes.string, // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl) url: PropTypes.string, // Whether the pill is in a message @@ -72,14 +81,20 @@ const Pill = React.createClass({ regex = REGEX_LOCAL_MATRIXTO; } - // Default to the empty array if no match for simplicity - // resource and prefix will be undefined instead of throwing - const matrixToMatch = regex.exec(nextProps.url) || []; + let matrixToMatch; + let resourceId; + let prefix; - const resourceId = matrixToMatch[1]; // The room/user ID - const prefix = matrixToMatch[2]; // The first character of prefix + if (nextProps.url) { + // Default to the empty array if no match for simplicity + // resource and prefix will be undefined instead of throwing + matrixToMatch = regex.exec(nextProps.url) || []; - const pillType = { + resourceId = matrixToMatch[1]; // The room/user ID + prefix = matrixToMatch[2]; // The first character of prefix + } + + const pillType = this.props.type || { '@': Pill.TYPE_USER_MENTION, '#': Pill.TYPE_ROOM_MENTION, '!': Pill.TYPE_ROOM_MENTION, @@ -88,6 +103,10 @@ const Pill = React.createClass({ let member; let room; switch (pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + room = nextProps.room; + } + break; case Pill.TYPE_USER_MENTION: { const localMember = nextProps.room.getMember(resourceId); member = localMember; @@ -160,6 +179,17 @@ const Pill = React.createClass({ let href = this.props.url; let onClick; switch (this.state.pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + const room = this.props.room; + if (room) { + linkText = "@room"; + if (this.props.shouldShowPillAvatar) { + avatar = ; + } + pillClass = 'mx_AtRoomPill'; + } + } + break; case Pill.TYPE_USER_MENTION: { // If this user is not a member of this room, default to the empty member const member = this.state.member; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 64b23238e5..faa4d6cf77 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -34,6 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import ContextualMenu from '../../structures/ContextualMenu'; import {RoomMember} from 'matrix-js-sdk'; import classNames from 'classnames'; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; linkifyMatrix(linkify); @@ -169,8 +170,10 @@ module.exports = React.createClass({ pillifyLinks: function(nodes) { const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + let node = nodes[0]; + while (node) { + let pillified = false; + if (node.tagName === "A" && node.getAttribute("href")) { const href = node.getAttribute("href"); @@ -189,10 +192,68 @@ module.exports = React.createClass({ ReactDOM.render(pill, pillContainer); node.parentNode.replaceChild(pillContainer, node); + // Pills within pills aren't going to go well, so move on + pillified = true; + } + } else if (node.nodeType == Node.TEXT_NODE) { + const Pill = sdk.getComponent('elements.Pill'); + + let currentTextNode = node; + const roomNotifTextNodes = []; + + // Take a textNode and break it up to make all the instances of @room their + // own textNode, adding those nodes to roomNotifTextNodes + while (currentTextNode !== null) { + const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); + let nextTextNode = null; + if (roomNotifPos > -1) { + let roomTextNode = currentTextNode; + + if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); + if (roomTextNode.textContent.length > Pill.roomNotifLen()) { + nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); + } + roomNotifTextNodes.push(roomTextNode); + } + currentTextNode = nextTextNode; + } + + if (roomNotifTextNodes.length > 0) { + const pushProcessor = new PushProcessor(MatrixClientPeg.get()); + const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); + if (pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { + // Now replace all those nodes with Pills + for (const roomNotifTextNode of roomNotifTextNodes) { + const pillContainer = document.createElement('span'); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const pill = ; + + ReactDOM.render(pill, pillContainer); + roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); + + // Set the next node to be processed to the one after the node + // we're adding now, since we've just inserted nodes into the structure + // we're iterating over. + // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once + node = roomNotifTextNode.nextSibling; + } + // Nothing else to do for a text node (and we don't need to advance + // the loop pointer because we did it above) + continue; + } } - } else if (node.children && node.children.length) { - this.pillifyLinks(node.children); } + + if (node.childNodes && node.childNodes.length && !pillified) { + this.pillifyLinks(node.childNodes); + } + + node = node.nextSibling; } }, From 7d7cd30e46b3da81821127b5860cd22acf744175 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 1 Nov 2017 22:10:03 +0000 Subject: [PATCH 46/49] turn NPE on flair resolution errors into a logged error --- src/stores/FlairStore.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 1ac518a4f6..d848ca7dda 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -129,7 +129,11 @@ class FlairStore extends EventEmitter { } const updatedUserGroups = resp.users; usersInFlight.forEach((userId) => { - this._usersPending[userId].resolve(updatedUserGroups[userId] || []); + if (this._usersPending[userId]) { + this._usersPending[userId].resolve(updatedUserGroups[userId] || []); + } else { + console.error("Promise vanished for resolving groups for " + userId); + } }); } From 790db94fd75ad29fa2e9f40ff948dd3d8c84ab7b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Nov 2017 13:25:55 +0000 Subject: [PATCH 47/49] Add toggle to alter the visibility of a room-group association --- src/components/views/groups/GroupRoomInfo.js | 94 +++++++++++++++++--- src/components/views/groups/GroupRoomTile.js | 2 +- src/groups.js | 1 + src/i18n/strings/en_EN.json | 12 +-- src/stores/GroupStore.js | 10 ++- 5 files changed, 98 insertions(+), 21 deletions(-) diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index 647651a0d8..bc1fc51853 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -21,7 +21,6 @@ import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import { GroupRoomType } from '../../../groups'; import GroupStoreCache from '../../../stores/GroupStoreCache'; import GeminiScrollbar from 'react-gemini-scrollbar'; @@ -34,13 +33,15 @@ module.exports = React.createClass({ propTypes: { groupId: PropTypes.string, - groupRoom: GroupRoomType, + groupRoomId: PropTypes.string, }, getInitialState: function() { return { - removingRoom: false, isUserPrivilegedInGroup: null, + groupRoom: null, + groupRoomPublicityLoading: false, + groupRoomRemoveLoading: false, }; }, @@ -55,6 +56,10 @@ module.exports = React.createClass({ } }, + componentWillUnmount() { + this._unregisterGroupStore(); + }, + _initGroupStore(groupId) { this._groupStore = GroupStoreCache.getGroupStore( this.context.matrixClient, this.props.groupId, @@ -68,15 +73,24 @@ module.exports = React.createClass({ } }, + _updateGroupRoom() { + this.setState({ + groupRoom: this._groupStore.getGroupRooms().find( + (r) => r.roomId === this.props.groupRoomId, + ), + }); + }, + onGroupStoreUpdated: function() { this.setState({ isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(), }); + this._updateGroupRoom(); }, _onRemove: function(e) { const groupId = this.props.groupId; - const roomName = this.props.groupRoom.displayname; + const roomName = this.state.groupRoom.displayname; e.preventDefault(); e.stopPropagation(); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -86,9 +100,9 @@ module.exports = React.createClass({ button: _t("Remove"), onFinished: (proceed) => { if (!proceed) return; - this.setState({removingRoom: true}); + this.setState({groupRoomRemoveLoading: true}); const groupId = this.props.groupId; - const roomId = this.props.groupRoom.roomId; + const roomId = this.props.groupRoomId; this._groupStore.removeRoomFromGroup(roomId).then(() => { dis.dispatch({ action: "view_group_room_list", @@ -103,7 +117,7 @@ module.exports = React.createClass({ ), }); }).finally(() => { - this.setState({removingRoom: false}); + this.setState({groupRoomRemoveLoading: false}); }); }, }); @@ -115,13 +129,41 @@ module.exports = React.createClass({ }); }, + _changeGroupRoomPublicity(e) { + const isPublic = e.target.value === "public"; + this.setState({ + groupRoomPublicityLoading: true, + }); + const groupId = this.props.groupId; + const roomId = this.props.groupRoomId; + const roomName = this.state.groupRoom.displayname; + this._groupStore.updateGroupRoomAssociation(roomId, isPublic).catch((err) => { + console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Something went wrong!"), + description: _t( + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.", + {roomName, groupId}, + ), + }); + }).finally(() => { + this.setState({ + groupRoomPublicityLoading: false, + }); + }); + }, + render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const EmojiText = sdk.getComponent('elements.EmojiText'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - if (this.state.removingRoom) { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) { const Spinner = sdk.getComponent("elements.Spinner"); - return ; + return
+ +
; } let adminTools; @@ -134,20 +176,46 @@ module.exports = React.createClass({ { _t('Remove from community') } +

+ { _t('Visibility in Room List') } + { this.state.groupRoomPublicityLoading ? + :
+ } +

+
+ +
+
+ +
; } const avatarUrl = this.context.matrixClient.mxcUrlToHttp( - this.props.groupRoom.avatarUrl, + this.state.groupRoom.avatarUrl, 36, 36, 'crop', ); - const groupRoomName = this.props.groupRoom.displayname; + const groupRoomName = this.state.groupRoom.displayname; const avatar = ; return (
- +
@@ -158,7 +226,7 @@ module.exports = React.createClass({
- { this.props.groupRoom.canonical_alias } + { this.state.groupRoom.canonical_alias }
diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index e445f06044..907ce93a4a 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -33,7 +33,7 @@ const GroupRoomTile = React.createClass({ dis.dispatch({ action: 'view_group_room', groupId: this.props.groupId, - groupRoom: this.props.groupRoom, + groupRoomId: this.props.groupRoom.roomId, }); }, diff --git a/src/groups.js b/src/groups.js index 3c80677b0c..6c266e0fb6 100644 --- a/src/groups.js +++ b/src/groups.js @@ -50,5 +50,6 @@ export function groupRoomFromApiObject(apiObject) { numJoinedMembers: apiObject.num_joined_members, worldReadable: apiObject.world_readable, guestCanJoin: apiObject.guest_can_join, + isPublic: apiObject.is_public !== false, }; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bffe3b3264..70e1af4834 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -158,6 +158,7 @@ "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "Failure to create room": "Failure to create room", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Unnamed Room": "Unnamed Room", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -328,7 +329,6 @@ "Rooms": "Rooms", "Low priority": "Low priority", "Historical": "Historical", - "Unnamed Room": "Unnamed Room", "a room": "a room", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", @@ -491,13 +491,15 @@ "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", - "Failed to remove room from community": "Failed to remove room from community", - "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Remove": "Remove", - "Remove this room from the community": "Remove this room from the community", + "Failed to remove room from community": "Failed to remove room from community", + "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", + "Visibility in Room List": "Visibility in Room List", + "Visible to everyone": "Visible to everyone", + "Only visible to group members": "Only visible to group members", + "Filter community rooms": "Filter community rooms", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Do you want to load widget from URL:": "Do you want to load widget from URL:", diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 3afac3c049..2578d373a7 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -141,9 +141,15 @@ export default class GroupStore extends EventEmitter { return this._summary.user ? this._summary.user.is_privileged : null; } - addRoomToGroup(roomId) { + addRoomToGroup(roomId, isPublic) { return this._matrixClient - .addRoomToGroup(this.groupId, roomId) + .addRoomToGroup(this.groupId, roomId, isPublic) + .then(this._fetchRooms.bind(this)); + } + + updateGroupRoomAssociation(roomId, isPublic) { + return this._matrixClient + .updateGroupRoomAssociation(this.groupId, roomId, isPublic) .then(this._fetchRooms.bind(this)); } From 982e87e01c67008761f486d313f7d546187e27ad Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Nov 2017 15:04:40 +0000 Subject: [PATCH 48/49] Communities are communities, wrap div for label alignment --- src/components/views/groups/GroupRoomInfo.js | 8 ++++++-- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index bc1fc51853..3f0b0067d2 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -189,7 +189,9 @@ module.exports = React.createClass({ checked={this.state.groupRoom.isPublic} onClick={this._changeGroupRoomPublicity} /> - { _t('Visible to everyone') } +
+ { _t('Visible to everyone') } +
@@ -199,7 +201,9 @@ module.exports = React.createClass({ checked={!this.state.groupRoom.isPublic} onClick={this._changeGroupRoomPublicity} /> - { _t('Only visible to group members') } +
+ { _t('Only visible to community members') } +
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0b5fdf678d..bc2f0754a7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -506,7 +506,7 @@ "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "The visibility of '%(roomName)s' in %(groupId)s could not be updated.", "Visibility in Room List": "Visibility in Room List", "Visible to everyone": "Visible to everyone", - "Only visible to group members": "Only visible to group members", + "Only visible to community members": "Only visible to community members", "Filter community rooms": "Filter community rooms", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", From 21e09840dc989646685546a909461d1bf5054a66 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Nov 2017 15:59:26 +0000 Subject: [PATCH 49/49] Fix multiple requests for publicised groups of given user Previously, a single user could end up in multiple batches, which would have been fine if the logic didn't assume otherwise. If a request took longer than 200ms, multiple batches would occur with intersecting sets of users, deleting promises that were then assumed to exist. The logic now takes all "in flight" users to also not be "pending". Pending now means that the user will be processed in the next batch. "In flight" means the user is part of an ongoing batch. --- src/stores/FlairStore.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index d848ca7dda..9424503390 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -66,7 +66,7 @@ class FlairStore extends EventEmitter { } // Bulk lookup ongoing, return promise to resolve/reject - if (this._usersPending[userId]) { + if (this._usersPending[userId] || this._usersInFlight[userId]) { return this._usersPending[userId].prom; } @@ -91,7 +91,7 @@ class FlairStore extends EventEmitter { console.error('Could not get groups for user', this.props.userId, err); throw err; }).finally(() => { - delete this._usersPending[userId]; + delete this._usersInFlight[userId]; }); // This debounce will allow consecutive requests for the public groups of users that @@ -113,27 +113,25 @@ class FlairStore extends EventEmitter { } async _batchedGetPublicGroups(matrixClient) { - // Take the userIds from the keys of this._usersPending - const usersInFlight = Object.keys(this._usersPending); + // Move users pending to users in flight + this._usersInFlight = this._usersPending; + this._usersPending = {}; + let resp = { users: [], }; try { - resp = await matrixClient.getPublicisedGroups(usersInFlight); + resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight)); } catch (err) { // Propagate the same error to all usersInFlight - usersInFlight.forEach((userId) => { - this._usersPending[userId].reject(err); + Object.keys(this._usersInFlight).forEach((userId) => { + this._usersInFlight[userId].reject(err); }); return; } const updatedUserGroups = resp.users; - usersInFlight.forEach((userId) => { - if (this._usersPending[userId]) { - this._usersPending[userId].resolve(updatedUserGroups[userId] || []); - } else { - console.error("Promise vanished for resolving groups for " + userId); - } + Object.keys(this._usersInFlight).forEach((userId) => { + this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []); }); }