From d5552e4a179ccd975367d412168430518779d797 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Sep 2019 17:51:27 +0100 Subject: [PATCH 01/55] Add bound 3PID warning when changing IS as well This extends the bound 3PID warning from the disconnect button to also appear when changing the IS as well. At the moment, the text is a bit terse, but will be improved separately. Fixes https://github.com/vector-im/riot-web/issues/10749 --- src/components/views/settings/SetIdServer.js | 134 ++++++++++++------- src/i18n/strings/en_EN.json | 6 +- 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a718d87fa6..e3d6d18c5b 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -137,7 +137,12 @@ export default class SetIdServer extends React.Component { MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: fullUrl, }); - this.setState({idServer: '', busy: false, error: null}); + this.setState({ + busy: false, + error: null, + currentClientIdServer: fullUrl, + idServer: '', + }); }; _checkIdServer = async (e) => { @@ -157,14 +162,34 @@ export default class SetIdServer extends React.Component { const authClient = new IdentityAuthClient(fullUrl); await authClient.getAccessToken(); + let save = true; + // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - this._showNoTermsWarning(fullUrl); - return; + save &= await this._showNoTermsWarning(fullUrl); } - this._saveIdServer(fullUrl); + // Show a general warning, possibly with details about any bound + // 3PIDs that would be left behind. + if (this.state.currentClientIdServer) { + save &= await this._showServerChangeWarning({ + title: _t("Change identity server"), + unboundMessage: _t( + "Disconnect from the identity server and " + + "connect to instead?", {}, + { + current: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + new: sub => {abbreviateUrl(this.state.idServer)}, + }, + ), + button: _t("Continue"), + }); + } + + if (save) { + this._saveIdServer(fullUrl); + } } catch (e) { console.error(e); if (e.cors === "rejected" || e.httpStatus === 404) { @@ -179,73 +204,80 @@ export default class SetIdServer extends React.Component { checking: false, error: errStr, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: this.state.idServer, }); }; _showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._saveIdServer(fullUrl); - }, + return new Promise(resolve => { + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), + onFinished: resolve, + }); }); } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); - - const boundThreepids = threepids.filter(tp => tp.bound); - let message; - if (boundThreepids.length) { - message = _t( - "You are currently sharing email addresses or phone numbers on the identity " + - "server . You will need to reconnect to to stop " + - "sharing them.", {}, - { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - }, - ); - } else { - message = _t( + const confirmed = await this._showServerChangeWarning({ + title: _t("Disconnect identity server"), + unboundMessage: _t( "Disconnect from the identity server ?", {}, {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, - ); - } - - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { - title: _t("Disconnect Identity Server"), - description: message, + ), button: _t("Disconnect"), - onFinished: (confirmed) => { - if (confirmed) { - this._disconnectIdServer(); - } - }, }); + if (confirmed) { + this._disconnectIdServer(); + } } finally { this.setState({disconnectBusy: false}); } }; + async _showServerChangeWarning({ title, unboundMessage, button }) { + const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); + + const boundThreepids = threepids.filter(tp => tp.bound); + let message; + if (boundThreepids.length) { + message = _t( + "You are currently sharing email addresses or phone numbers on the identity " + + "server . You will need to reconnect to to stop " + + "sharing them.", {}, + { + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + // XXX: https://github.com/vector-im/riot-web/issues/9086 + idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + }, + ); + } else { + message = unboundMessage; + } + + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + return new Promise(resolve => { + Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { + title, + description: message, + button, + onFinished: resolve, + }); + }); + } + _disconnectIdServer = () => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dd0894b813..f31fcc7157 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -554,14 +554,16 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", + "Change identity server": "Change identity server", + "Disconnect from the identity server and connect to instead?": "Disconnect from the identity server and connect to instead?", "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "Identity server has no terms of service": "Identity server has no terms of service", "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", - "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", + "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", - "Disconnect Identity Server": "Disconnect Identity Server", "Disconnect": "Disconnect", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", From af35cdc2ea87fe38b8fd70996cf3e5279918feea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 5 Sep 2019 20:30:19 -0600 Subject: [PATCH 02/55] Support sending hidden read receipts Fixes https://github.com/vector-im/riot-web/issues/2527 --- src/components/structures/TimelinePanel.js | 5 +++++ .../views/settings/tabs/user/LabsUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 7 +++++++ 4 files changed, 14 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index cdeea78204..44569569b6 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -685,20 +685,25 @@ const TimelinePanel = createReactClass({ } this.lastRMSentEventId = this.state.readMarkerEventId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts"); + debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, 'rm', this.state.readMarkerEventId, lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ' hidden:' + hiddenRR, ); MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent, // Could be null, in which case no RR is sent + {hidden: hiddenRR}, ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { return MatrixClientPeg.get().sendReadReceipt( lastReadEvent, + {hidden: hiddenRR}, ).catch((e) => { console.error(e); this.lastRRSentEventId = undefined; diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 9c2d49a8bf..07a2bf722a 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -54,6 +54,7 @@ export default class LabsUserSettingsTab extends React.Component { + ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dd0894b813..acccade834 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -367,6 +367,7 @@ "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", + "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 70abf406b8..f86a8566c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -394,4 +394,11 @@ export const SETTINGS = { // This is a tri-state value, where `null` means "prompt the user". default: null, }, + "sendReadReceipts": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td( + "Send read receipts for messages (requires compatible homeserver to disable)", + ), + default: true, + }, }; From 2ff592c54259757aa4a2fcda8ad898543a4a019d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 10:48:24 +0100 Subject: [PATCH 03/55] Use existing modal promises --- src/components/views/settings/SetIdServer.js | 52 ++++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index e3d6d18c5b..ebd97cebc1 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -167,13 +167,14 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - save &= await this._showNoTermsWarning(fullUrl); + const [confirmed] = await this._showNoTermsWarning(fullUrl); + save &= confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - if (this.state.currentClientIdServer) { - save &= await this._showServerChangeWarning({ + if (save && this.state.currentClientIdServer) { + const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + @@ -185,6 +186,7 @@ export default class SetIdServer extends React.Component { ), button: _t("Continue"), }); + save &= confirmed; } if (save) { @@ -209,29 +211,27 @@ export default class SetIdServer extends React.Component { _showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - return new Promise(resolve => { - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: resolve, - }); + const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), }); + return finished; } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const confirmed = await this._showServerChangeWarning({ + const [confirmed] = await this._showServerChangeWarning({ title: _t("Disconnect identity server"), unboundMessage: _t( "Disconnect from the identity server ?", {}, @@ -268,14 +268,12 @@ export default class SetIdServer extends React.Component { } const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - return new Promise(resolve => { - Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { - title, - description: message, - button, - onFinished: resolve, - }); + const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { + title, + description: message, + button, }); + return finished; } _disconnectIdServer = () => { From 19fff51b01a1494dfca432d00ed3c0a91cdbad61 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 11:11:54 +0100 Subject: [PATCH 04/55] Rework handling for terms CORS error Earlier changes in this branch removed the "next step" of saving from the dialogs, so we need to fold in the CORS error case. --- src/components/views/settings/SetIdServer.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ebd97cebc1..20524d8ae3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -165,7 +165,18 @@ export default class SetIdServer extends React.Component { let save = true; // Double check that the identity server even has terms of service. - const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + let terms; + try { + terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + } catch (e) { + console.error(e); + if (e.cors === "rejected" || e.httpStatus === 404) { + terms = null; + } else { + throw e; + } + } + if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { const [confirmed] = await this._showNoTermsWarning(fullUrl); save &= confirmed; @@ -194,10 +205,6 @@ export default class SetIdServer extends React.Component { } } catch (e) { console.error(e); - if (e.cors === "rejected" || e.httpStatus === 404) { - this._showNoTermsWarning(fullUrl); - return; - } errStr = _t("Terms of service not accepted or the identity server is invalid."); } } From df03477907dd7173a9fdf983ab592f9d2346f2a8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 11:18:25 +0100 Subject: [PATCH 05/55] Show change warning only when different from current --- src/components/views/settings/SetIdServer.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 20524d8ae3..6460ad4b1f 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -147,10 +147,11 @@ export default class SetIdServer extends React.Component { _checkIdServer = async (e) => { e.preventDefault(); + const { idServer, currentClientIdServer } = this.state; this.setState({busy: true, checking: true, error: null}); - const fullUrl = unabbreviateUrl(this.state.idServer); + const fullUrl = unabbreviateUrl(idServer); let errStr = await checkIdentityServerUrl(fullUrl); if (!errStr) { @@ -179,25 +180,26 @@ export default class SetIdServer extends React.Component { if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { const [confirmed] = await this._showNoTermsWarning(fullUrl); - save &= confirmed; + save = confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - if (save && this.state.currentClientIdServer) { + console.log(fullUrl, idServer, currentClientIdServer) + if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + "connect to instead?", {}, { - current: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - new: sub => {abbreviateUrl(this.state.idServer)}, + current: sub => {abbreviateUrl(currentClientIdServer)}, + new: sub => {abbreviateUrl(idServer)}, }, ), button: _t("Continue"), }); - save &= confirmed; + save = confirmed; } if (save) { From 11a5fca7024b8f489c39aaebf773f6e07e931908 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 13:44:44 +0100 Subject: [PATCH 06/55] Remove logs --- src/components/views/settings/SetIdServer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 6460ad4b1f..d3fc944a70 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -185,7 +185,6 @@ export default class SetIdServer extends React.Component { // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - console.log(fullUrl, idServer, currentClientIdServer) if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), From 5fddb20d860ad51b2051fa3b12bae46d138d174b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 14:27:20 +0100 Subject: [PATCH 07/55] Stop setting IS input field on account change This stops setting a value in the IS input on account change. While it may have been marginally useful if you have the form open and change on a different device, it also seems to pick up changes on the current device, leading to strange UX locally. Fixes https://github.com/vector-im/riot-web/issues/10756 Fixes https://github.com/vector-im/riot-web/issues/10757 --- src/components/views/settings/SetIdServer.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a718d87fa6..af38c997a3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -93,18 +93,11 @@ export default class SetIdServer extends React.Component { onAction = (payload) => { // We react to changes in the ID server in the event the user is staring at this form - // when changing their identity server on another device. If the user is trying to change - // it in two places, we'll end up stomping all over their input, but at that point we - // should question our UX which led to them doing that. + // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; - const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); - let abbr = ''; - if (fullUrl) abbr = abbreviateUrl(fullUrl); - this.setState({ - currentClientIdServer: fullUrl, - idServer: abbr, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), }); }; From 3ad88604cc32e95a92a43af054790a8c2d6408d9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 13:43:21 +0100 Subject: [PATCH 08/55] Stregthen bound 3PID warning dialog This tweaks the bound 3PID text and adds danger styling. Fixes https://github.com/vector-im/riot-web/issues/10750 --- src/components/views/settings/SetIdServer.js | 16 +++++++++++----- src/i18n/strings/en_EN.json | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index d3fc944a70..afb2d62e0e 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -260,17 +260,21 @@ export default class SetIdServer extends React.Component { const boundThreepids = threepids.filter(tp => tp.bound); let message; + let danger = false; if (boundThreepids.length) { message = _t( - "You are currently sharing email addresses or phone numbers on the identity " + - "server . You will need to reconnect to to stop " + - "sharing them.", {}, + "You are still sharing your personal data on the identity " + + "server .

" + + "We recommend that you remove your email addresses and phone numbers " + + "from the identity server before disconnecting.", {}, { idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + b: sub => {sub}, + br: () =>
, }, ); + danger = true; + button = _t("Disconnect anyway"); } else { message = unboundMessage; } @@ -280,6 +284,8 @@ export default class SetIdServer extends React.Component { title, description: message, button, + cancelButton: _t("Go back"), + danger, }); return finished; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f31fcc7157..02fd05b067 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -563,7 +563,9 @@ "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", - "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", + "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", + "Disconnect anyway": "Disconnect anyway", + "Go back": "Go back", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", @@ -1287,7 +1289,6 @@ "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", - "Go back": "Go back", "Room Settings - %(roomName)s": "Room Settings - %(roomName)s", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", From 034c35b07e34668de22618bf7101e283455f24ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 19:19:06 +0100 Subject: [PATCH 09/55] Switch to React 16.9 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 6 ++-- yarn.lock | 86 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index b4e1af9f0a..70de250830 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,10 @@ "qrcode-react": "^0.1.16", "qs": "^6.6.0", "querystring": "^0.2.0", - "react": "^15.6.0", - "react-addons-css-transition-group": "15.3.2", + "react": "^16.9.0", + "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", - "react-dom": "^15.6.0", + "react-dom": "^16.9.0", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#f644523", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/yarn.lock b/yarn.lock index 1989f4339a..9cfc36615a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1857,6 +1857,11 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.4.tgz#9cf2de494ca84060a2a8d2854edd6dfb0445f386" integrity sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w== +chain-function@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc" + integrity sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg== + chalk@2.4.2, "chalk@^1.1.3 || 2.x", chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2562,6 +2567,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -6165,7 +6177,7 @@ promise@^7.0.3, promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6344,10 +6356,12 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-css-transition-group@15.3.2: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667" - integrity sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc= +react-addons-css-transition-group@15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.2.tgz#9e4376bcf40b5217d14ec68553081cee4b08a6d6" + integrity sha1-nkN2vPQLUhfRTsaFUwgc7ksIptY= + dependencies: + react-transition-group "^1.2.0" react-beautiful-dnd@^4.0.1: version "4.0.1" @@ -6365,16 +6379,6 @@ react-beautiful-dnd@^4.0.1: redux-thunk "^2.2.0" reselect "^3.0.1" -react-dom@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" - integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= - dependencies: - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - react-dom@^16.4.2: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" @@ -6385,6 +6389,16 @@ react-dom@^16.4.2: prop-types "^15.6.2" scheduler "^0.13.6" +react-dom@^16.9.0: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962" + integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.15.0" + "react-gemini-scrollbar@github:matrix-org/react-gemini-scrollbar#f644523": version "2.1.5" resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c" @@ -6428,16 +6442,16 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" - integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= +react-transition-group@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q== dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" react@^16.4.2: version "16.8.6" @@ -6449,6 +6463,15 @@ react@^16.4.2: prop-types "^15.6.2" scheduler "^0.13.6" +react@^16.9.0: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa" + integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -6901,6 +6924,14 @@ scheduler@^0.13.6: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e" + integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -8134,6 +8165,13 @@ walk@^2.3.9: dependencies: foreachasync "^3.0.0" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + dependencies: + loose-envify "^1.0.0" + watchpack@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" From e29184ae1d43934e36f5b067e520b624a0e39cb4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 6 Sep 2019 13:02:18 -0600 Subject: [PATCH 10/55] Support secret per-room hidden read receipts --- src/components/structures/TimelinePanel.js | 3 ++- src/settings/Settings.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 44569569b6..0ca1cb9996 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -685,7 +685,8 @@ const TimelinePanel = createReactClass({ } this.lastRMSentEventId = this.state.readMarkerEventId; - const hiddenRR = !SettingsStore.getValue("sendReadReceipts"); + const roomId = this.props.timelineSet.room.roomId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId); debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f86a8566c6..7b049208aa 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -395,7 +395,7 @@ export const SETTINGS = { default: null, }, "sendReadReceipts": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, + supportedLevels: LEVELS_ROOM_SETTINGS, displayName: _td( "Send read receipts for messages (requires compatible homeserver to disable)", ), From ef2ff31a46b4f016772072c02802d7bad70683ba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 09:34:08 +0100 Subject: [PATCH 11/55] Fix replying from search results for this and all rooms Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 6 +++++- src/components/views/rooms/ReplyPreview.js | 9 ++++++--- src/stores/RoomViewStore.js | 21 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 543b35eaf8..195e4130a6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -586,7 +586,6 @@ module.exports = createReactClass({ return ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, MatrixClientPeg.get(), ); - break; case 'notifier_enabled': case 'upload_started': case 'upload_finished': @@ -624,6 +623,11 @@ module.exports = createReactClass({ showApps: payload.show, }); break; + case 'reply_to_event': + if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) { + this.onCancelSearchClick(); + } + break; } }, diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 3b7874a875..58e7237801 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -37,18 +37,19 @@ export default class ReplyPreview extends React.Component { constructor(props, context) { super(props, context); + this.unmounted = false; this.state = { - event: null, + event: RoomViewStore.getQuotingEvent(), }; this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); - this._onRoomViewStoreUpdate(); } componentWillUnmount() { + this.unmounted = true; + // Remove RoomStore listener if (this._roomStoreToken) { this._roomStoreToken.remove(); @@ -56,6 +57,8 @@ export default class ReplyPreview extends React.Component { } _onRoomViewStoreUpdate() { + if (this.unmounted) return; + const event = RoomViewStore.getQuotingEvent(); if (this.state.event !== event) { this.setState({ event }); diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 166833325e..7e1b06c0bf 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -113,9 +113,19 @@ class RoomViewStore extends Store { }); break; case 'reply_to_event': - this._setState({ - replyingToEvent: payload.event, - }); + // If currently viewed room does not match the room in which we wish to reply then change rooms + // this can happen when performing a search across all rooms + if (payload.event && payload.event.getRoomId() !== this._state.roomId) { + dis.dispatch({ + action: 'view_room', + room_id: payload.event.getRoomId(), + replyingToEvent: payload.event, + }); + } else { + this._setState({ + replyingToEvent: payload.event, + }); + } break; case 'open_room_settings': { const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); @@ -147,6 +157,11 @@ class RoomViewStore extends Store { isEditingSettings: false, }; + // Allow being given an event to be replied to when switching rooms but sanity check its for this room + if (payload.replyingToEvent && payload.replyingToEvent.getRoomId() === payload.room_id) { + newState.replyingToEvent = payload.replyingToEvent; + } + if (this._state.forwardingEvent) { dis.dispatch({ action: 'send_event', From c3adddb5ac41ec1516f275a8c1b33eb676fafe1e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 9 Sep 2019 10:27:02 +0100 Subject: [PATCH 12/55] Change to paragraphs outside the strings --- src/components/views/settings/SetIdServer.js | 25 +++++++++++--------- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index afb2d62e0e..af7416518d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -262,17 +262,20 @@ export default class SetIdServer extends React.Component { let message; let danger = false; if (boundThreepids.length) { - message = _t( - "You are still sharing your personal data on the identity " + - "server .

" + - "We recommend that you remove your email addresses and phone numbers " + - "from the identity server before disconnecting.", {}, - { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - b: sub => {sub}, - br: () =>
, - }, - ); + message =
+

{_t( + "You are still sharing your personal data on the identity " + + "server .", {}, + { + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + b: sub => {sub}, + }, + )}

+

{_t( + "We recommend that you remove your email addresses and phone numbers " + + "from the identity server before disconnecting.", + )}

+
; danger = true; button = _t("Disconnect anyway"); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02fd05b067..26db20fe93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -563,7 +563,8 @@ "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", - "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", + "You are still sharing your personal data on the identity server .": "You are still sharing your personal data on the identity server .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", "Disconnect anyway": "Disconnect anyway", "Go back": "Go back", "Identity Server (%(server)s)": "Identity Server (%(server)s)", From 088f9e4cc5670cf93a17d6a9f0a2f6ff9c6e4964 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 12:50:05 +0100 Subject: [PATCH 13/55] Catch error from changing room power level requirements and show modal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/tabs/room/RolesRoomSettingsTab.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index 002748694c..64f3992ad0 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -148,7 +148,18 @@ export default class RolesRoomSettingsTab extends React.Component { parentObj[keyPath[keyPath.length - 1]] = value; } - client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent); + client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, { + title: _t('Error changing power level requirement'), + description: _t( + "An error occurred changing the room's power level requirements. Ensure you have sufficient " + + "permissions and try again.", + ), + }); + }); }; _onUserPowerLevelChanged = (value, powerLevelKey) => { From 9c488426cc6a20fd905cdabe3acf2d1ab30fdbb2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 12:51:30 +0100 Subject: [PATCH 14/55] add i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6529e7322c..5c7a60f58b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -690,6 +690,8 @@ "Failed to unban": "Failed to unban", "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", + "Error changing power level requirement": "Error changing power level requirement", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", "Error changing power level": "Error changing power level", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.", "Default role": "Default role", From f205ddbc8f2c249154a5c0672e58342c18cc9211 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Sep 2019 15:10:50 +0200 Subject: [PATCH 15/55] add redact recent messages button in member info --- src/components/views/rooms/MemberInfo.js | 86 +++++++++++++++++++++++- src/i18n/strings/en_EN.json | 7 ++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 26f731d308..f82f655349 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -48,6 +48,8 @@ import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import Matrix from "matrix-js-sdk"; +const EventTimeline = Matrix.EventTimeline; module.exports = createReactClass({ displayName: 'MemberInfo', @@ -64,6 +66,7 @@ module.exports = createReactClass({ mute: false, modifyLevel: false, synapseDeactivate: false, + redactMessages: false, }, muted: false, isTargetMod: false, @@ -356,6 +359,73 @@ module.exports = createReactClass({ }); }, + onRedactAllMessages: async function() { + const {roomId, userId} = this.props.member; + const room = this.context.matrixClient.getRoom(roomId); + if (!room) { + return; + } + let timeline = room.getLiveTimeline(); + let eventsToRedact = []; + while (timeline) { + eventsToRedact = timeline.getEvents().reduce((events, event) => { + if (event.getSender() === userId && !event.isRedacted()) { + return events.concat(event); + } else { + return events; + } + }, eventsToRedact); + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + const count = eventsToRedact.length; + const user = this.props.member.name; + + if (count === 0) { + const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); + Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { + title: _t("No recent messages by %(user)s found", {user}), + description: +
+

{ _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

+
, + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const confirmed = await new Promise((resolve) => { + Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { + title: _t("Remove recent messages by %(user)s", {user}), + description: +
+

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

+

{ _t("For large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+
, + button: _t("Remove %(count)s messages", {count}), + onFinished: resolve, + }); + }); + + if (!confirmed) { + return; + } + + // Submitting a large number of redactions freezes the UI, + // so first wait 200ms to allow to rerender after closing the dialog. + await new Promise(resolve => setTimeout(resolve, 200)); + + await Promise.all(eventsToRedact.map(async event => { + try { + await this.context.matrixClient.redactEvent(roomId, event.getId()); + } catch (err) { + // log and swallow errors + console.error("Could not redact", event.getId()); + console.error(err); + } + })); + console.log("Done redacting recent messages!"); + } + }, + _warnSelfDemote: function() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); return new Promise((resolve) => { @@ -602,6 +672,7 @@ module.exports = createReactClass({ mute: false, modifyLevel: false, modifyLevelMax: 0, + redactMessages: false, }; // Calculate permissions for Synapse before doing the PL checks @@ -623,6 +694,7 @@ module.exports = createReactClass({ can.mute = me.powerLevel >= editPowerLevel; can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel); can.modifyLevelMax = me.powerLevel; + can.redactMessages = me.powerLevel >= powerLevels.redact; return can; }, @@ -812,6 +884,7 @@ module.exports = createReactClass({ let banButton; let muteButton; let giveModButton; + let redactButton; let synapseDeactivateButton; let spinner; @@ -892,6 +965,16 @@ module.exports = createReactClass({ ); } + + + if (this.state.can.redactMessages) { + redactButton = ( + + { _t("Remove recent messages") } + + ); + } + if (this.state.can.ban) { let label = _t("Ban"); if (this.props.member.membership === 'ban') { @@ -932,7 +1015,7 @@ module.exports = createReactClass({ } let adminTools; - if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton) { + if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) { adminTools =

{ _t("Admin Tools") }

@@ -941,6 +1024,7 @@ module.exports = createReactClass({ { muteButton } { kickButton } { banButton } + { redactButton } { giveModButton } { synapseDeactivateButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ed7b180312..fac4d53061 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -794,6 +794,12 @@ "Unban this user?": "Unban this user?", "Ban this user?": "Ban this user?", "Failed to ban user": "Failed to ban user", + "No recent messages by %(user)s found": "No recent messages by %(user)s found", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", + "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", + "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "Remove %(count)s messages|other": "Remove %(count)s messages", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "Demote": "Demote", @@ -813,6 +819,7 @@ "Share Link to User": "Share Link to User", "User Options": "User Options", "Direct chats": "Direct chats", + "Remove recent messages": "Remove recent messages", "Unmute": "Unmute", "Mute": "Mute", "Revoke Moderator": "Revoke Moderator", From 3edf345b025125ce211964eed63d652cea4cffb3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Sep 2019 16:19:10 +0200 Subject: [PATCH 16/55] PR feedback --- src/components/views/rooms/MemberInfo.js | 13 ++++++------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index f82f655349..cb11cf39ad 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -48,8 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; -import Matrix from "matrix-js-sdk"; -const EventTimeline = Matrix.EventTimeline; +import {EventTimeline} from "matrix-js-sdk"; module.exports = createReactClass({ displayName: 'MemberInfo', @@ -398,7 +397,7 @@ module.exports = createReactClass({ description:

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

-

{ _t("For large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+

{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

, button: _t("Remove %(count)s messages", {count}), onFinished: resolve, @@ -410,9 +409,10 @@ module.exports = createReactClass({ } // Submitting a large number of redactions freezes the UI, - // so first wait 200ms to allow to rerender after closing the dialog. - await new Promise(resolve => setTimeout(resolve, 200)); + // so first yield to allow to rerender after closing the dialog. + await Promise.resolve(); + console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); await Promise.all(eventsToRedact.map(async event => { try { await this.context.matrixClient.redactEvent(roomId, event.getId()); @@ -422,7 +422,7 @@ module.exports = createReactClass({ console.error(err); } })); - console.log("Done redacting recent messages!"); + console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); } }, @@ -966,7 +966,6 @@ module.exports = createReactClass({ ); } - if (this.state.can.redactMessages) { redactButton = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fac4d53061..3f0787523c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -798,7 +798,7 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", - "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Remove %(count)s messages|other": "Remove %(count)s messages", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", From 0b56e7a81c1f54b675a02d6b3f3bab637d90d270 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:26:10 +0100 Subject: [PATCH 17/55] Support Synapse deactivate on MemberInfo without Room (timeline pill) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MemberInfo.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index cb11cf39ad..dc9f1070fd 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -642,7 +642,10 @@ module.exports = createReactClass({ _calculateOpsPermissions: async function(member) { const defaultPerms = { - can: {}, + can: { + // Calculate permissions for Synapse before doing the PL checks + synapseDeactivate: await this.context.matrixClient.isSynapseAdministrator(), + }, muted: false, }; const room = this.context.matrixClient.getRoom(member.roomId); @@ -656,9 +659,10 @@ module.exports = createReactClass({ const them = member; return { - can: await this._calculateCanPermissions( - me, them, powerLevels.getContent(), - ), + can: { + ...defaultPerms.can, + ...await this._calculateCanPermissions(me, them, powerLevels.getContent()), + }, muted: this._isMuted(them, powerLevels.getContent()), isTargetMod: them.powerLevel > powerLevels.getContent().users_default, }; @@ -675,9 +679,6 @@ module.exports = createReactClass({ redactMessages: false, }; - // Calculate permissions for Synapse before doing the PL checks - can.synapseDeactivate = await this.context.matrixClient.isSynapseAdministrator(); - const canAffectUser = them.powerLevel < me.powerLevel || isMe; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); From 80add7be927c8d59eced97ffc4e88ebbb941df8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:38:51 +0100 Subject: [PATCH 18/55] delint more properly Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 195e4130a6..3446901331 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -583,9 +583,10 @@ module.exports = createReactClass({ payload.data.description || payload.data.name); break; case 'picture_snapshot': - return ContentMessages.sharedInstance().sendContentListToRoom( + ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, MatrixClientPeg.get(), ); + break; case 'notifier_enabled': case 'upload_started': case 'upload_finished': From 80dd5a1b0a576bcf4f3d7c242e3da9c9e94ba52f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:53:55 +0200 Subject: [PATCH 19/55] add explore button next to filter field --- res/css/structures/_LeftPanel.scss | 41 +++++++++++ res/css/structures/_SearchBox.scss | 21 +++--- res/css/views/rooms/_RoomList.scss | 4 -- res/img/explore.svg | 97 ++++++++++++++++++++++++++ src/components/structures/LeftPanel.js | 15 +++- src/i18n/strings/en_EN.json | 1 + 6 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 res/img/explore.svg diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d10fdb6d6..e01dfb75cd 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -125,3 +125,44 @@ limitations under the License. margin-top: 12px; } } + +.mx_LeftPanel_exploreAndFilterRow { + display: flex; + + .mx_SearchBox { + flex: 1 1 0; + min-width: 0; + } +} + +.mx_LeftPanel_explore { + flex: 0 0 40%; + overflow: hidden; + box-sizing: border-box; + + .mx_AccessibleButton { + font-size: 14px; + margin: 9px; + margin-right: 0; + padding: 9px; + padding-left: 42px; + font-weight: 600; + color: $notice-secondary-color; + position: relative; + border-radius: 4px; + + &::before { + cursor: pointer; + mask: url('$(res)/img/explore.svg'); + mask-repeat: no-repeat; + mask-position: center center; + content: ""; + left: 14px; + top: 10px; + width: 16px; + height: 16px; + background-color: $notice-secondary-color; + position: absolute; + } + } +} diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss index 9434d93bd2..7d13405478 100644 --- a/res/css/structures/_SearchBox.scss +++ b/res/css/structures/_SearchBox.scss @@ -14,12 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SearchBox_closeButton { - cursor: pointer; - background-image: url('$(res)/img/icons-close.svg'); - background-repeat: no-repeat; - width: 16px; - height: 16px; - background-position: center; - padding: 9px; +.mx_SearchBox { + flex: 1 1 0; + min-width: 0; + + .mx_SearchBox_closeButton { + cursor: pointer; + background-image: url('$(res)/img/icons-close.svg'); + background-repeat: no-repeat; + width: 16px; + height: 16px; + background-position: center; + padding: 9px; + } } diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index b51d720e4d..5ed22f997d 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -27,10 +27,6 @@ limitations under the License. position: relative; } -.mx_SearchBox { - flex: none; -} - /* hide resize handles next to collapsed / empty sublists */ .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { display: none; diff --git a/res/img/explore.svg b/res/img/explore.svg new file mode 100644 index 0000000000..3956e912ac --- /dev/null +++ b/res/img/explore.svg @@ -0,0 +1,97 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f083e5ab38..ab1190d8b9 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -217,6 +217,7 @@ const LeftPanel = createReactClass({ const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); const SearchBox = sdk.getComponent('structures.SearchBox'); const CallPreview = sdk.getComponent('voip.CallPreview'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); let tagPanelContainer; @@ -240,6 +241,15 @@ const LeftPanel = createReactClass({ }, ); + let exploreButton; + if (!this.props.collapsed) { + exploreButton = ( +
+ dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")} +
+ ); + } + const searchBox = ( { breadcrumbs } - { searchBox } +
+ { exploreButton } + { searchBox } +
Date: Tue, 10 Sep 2019 10:57:25 +0200 Subject: [PATCH 20/55] hide explore button when focusing filter field --- res/css/structures/_LeftPanel.scss | 5 +++++ src/components/structures/LeftPanel.js | 13 +++++++++++++ src/components/structures/SearchBox.js | 10 ++++++++++ 3 files changed, 28 insertions(+) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index e01dfb75cd..c2b8a6873f 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -138,8 +138,13 @@ limitations under the License. .mx_LeftPanel_explore { flex: 0 0 40%; overflow: hidden; + transition: flex-basis 0.2s; box-sizing: border-box; + &.mx_LeftPanel_explore_hidden { + flex-basis: 0; + } + .mx_AccessibleButton { font-size: 14px; margin: 9px; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index ab1190d8b9..b77b5e2e39 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -81,6 +81,9 @@ const LeftPanel = createReactClass({ if (this.state.searchFilter !== nextState.searchFilter) { return true; } + if (this.state.searchFocused !== nextState.searchFocused) { + return true; + } return false; }, @@ -209,6 +212,14 @@ const LeftPanel = createReactClass({ this._roomList = ref; }, + _onSearchFocus: function() { + this.setState({searchFocused: true}); + }, + + _onSearchBlur: function() { + this.setState({searchFocused: false}); + }, + render: function() { const RoomList = sdk.getComponent('rooms.RoomList'); const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); @@ -255,6 +266,8 @@ const LeftPanel = createReactClass({ placeholder={ _t('Filter room names') } onSearch={ this.onSearch } onCleared={ this.onSearchCleared } + onFocus={this._onSearchFocus} + onBlur={this._onSearchBlur} collapsed={this.props.collapsed} />); let breadcrumbs; diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index d8ff08adbf..70e898fe04 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -95,6 +95,15 @@ module.exports = createReactClass({ _onFocus: function(ev) { ev.target.select(); + if (this.props.onFocus) { + this.props.onFocus(ev); + } + }, + + _onBlur: function(ev) { + if (this.props.onBlur) { + this.props.onBlur(ev); + } }, _clearSearch: function(source) { @@ -132,6 +141,7 @@ module.exports = createReactClass({ onChange={ this.onChange } onKeyDown={ this._onKeyDown } placeholder={ this.props.placeholder } + onBlur={this._onBlur} /> { clearButton } From 15d37746651d9b9f8162859539d7f8ebcfc14f39 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:58:11 +0200 Subject: [PATCH 21/55] show shorter placeholder for filter feed when not focused --- src/components/structures/LeftPanel.js | 3 ++- src/components/structures/SearchBox.js | 12 +++++++++++- src/i18n/strings/en_EN.json | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index b77b5e2e39..4c5b1686f5 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -263,7 +263,8 @@ const LeftPanel = createReactClass({ const searchBox = ( {this._clearSearch("button"); } }>
) : undefined; + // show a shorter placeholder when blurred, if requested + // this is used for the room filter field that has + // the explore button next to it when blurred + const placeholder = this.state.blurred ? + (this.props.blurredPlaceholder || this.props.placeholder) : + this.props.placeholder; const className = this.props.className || ""; return (
@@ -140,8 +150,8 @@ module.exports = createReactClass({ onFocus={ this._onFocus } onChange={ this.onChange } onKeyDown={ this._onKeyDown } - placeholder={ this.props.placeholder } onBlur={this._onBlur} + placeholder={ placeholder } /> { clearButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b7220e3082..48a9e25857 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1549,8 +1549,9 @@ "Community %(groupId)s not found": "Community %(groupId)s not found", "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", - "Filter room names": "Filter room names", "Explore": "Explore", + "Filter": "Filter", + "Filter rooms…": "Filter rooms…", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", From 1c4093eb0f3b37701b1a7dd6e726bbfbb989f6ca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:58:44 +0200 Subject: [PATCH 22/55] make filter feed transparent when not focussed --- res/css/structures/_SearchBox.scss | 4 ++++ src/components/structures/SearchBox.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss index 7d13405478..23ee06f7b3 100644 --- a/res/css/structures/_SearchBox.scss +++ b/res/css/structures/_SearchBox.scss @@ -18,6 +18,10 @@ limitations under the License. flex: 1 1 0; min-width: 0; + &.mx_SearchBox_blurred:not(:hover) { + background-color: transparent; + } + .mx_SearchBox_closeButton { cursor: pointer; background-image: url('$(res)/img/icons-close.svg'); diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index a381416a80..4d68ff4e96 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -140,7 +140,7 @@ module.exports = createReactClass({ this.props.placeholder; const className = this.props.className || ""; return ( -
+
Date: Tue, 10 Sep 2019 10:59:22 +0200 Subject: [PATCH 23/55] make explore button white on hover --- res/css/structures/_LeftPanel.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index c2b8a6873f..f83195f847 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -156,6 +156,10 @@ limitations under the License. position: relative; border-radius: 4px; + &:hover { + background-color: $primary-bg-color; + } + &::before { cursor: pointer; mask: url('$(res)/img/explore.svg'); From 4148f1697cd6bab2d183030f653d112c80d84572 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:59:36 +0200 Subject: [PATCH 24/55] make input fields on a dark panel have a white background (filter field) --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index adf4c93290..77b474a013 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -171,7 +171,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search], .mx_textinput { color: $input-darker-fg-color; - background-color: $input-darker-bg-color; + background-color: $primary-bg-color; border: none; } } From 68dde07f49d5be72caf54e38720715e46f750060 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 11:59:59 +0200 Subject: [PATCH 25/55] make add room button go to create room dialog instead of room directory --- src/components/structures/RoomDirectory.js | 11 ----------- src/components/views/rooms/RoomList.js | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index aa2e56d3c2..13ae83b067 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -321,11 +321,6 @@ module.exports = createReactClass({ } }, - onCreateRoomClicked: function() { - this.props.onFinished(); - dis.dispatch({action: 'view_create_room'}); - }, - onJoinClick: function(alias) { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId) { @@ -602,17 +597,11 @@ module.exports = createReactClass({
; } - const createRoomButton = ({_t("Create new room")}); - return (
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2454f012f8..da2d11f34b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -758,7 +758,7 @@ module.exports = createReactClass({ headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), - onAddRoom: () => {dis.dispatch({action: 'view_room_directory'})}, + onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, }, ]; const tagSubLists = Object.keys(this.state.lists) From 4fbedec013b006da8de517b7d47f5e2a69be67ab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:01:20 -0600 Subject: [PATCH 26/55] Make uses of AddressPickerDialog static dialogs Fixes https://github.com/vector-im/riot-web/issues/10603 Static dialogs are ones that stay open underneath other dialogs, like the terms of service prompt. This is how user/room settings operate. --- src/GroupAddressPicker.js | 4 ++-- src/RoomInvite.js | 4 ++-- src/components/structures/GroupView.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index cd5ecc790d..dfc90841a3 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -46,7 +46,7 @@ export function showGroupInviteDialog(groupId) { _onGroupInviteFinished(groupId, addrs).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } @@ -81,7 +81,7 @@ export function showGroupAddRoomDialog(groupId) { _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 856a2ca577..ec99cd8e9a 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -62,7 +62,7 @@ export function showStartChatInviteDialog() { validAddressTypes, button: _t("Start Chat"), onFinished: _onStartDmFinished, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } export function showRoomInviteDialog(roomId) { @@ -88,7 +88,7 @@ export function showRoomInviteDialog(roomId) { onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } /** diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed18f0f463..70d8b2e298 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -120,7 +120,7 @@ const CategoryRoomList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { @@ -297,7 +297,7 @@ const RoleUserList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { From 312143315bba2a8863cc9357858199a175ac62d5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:13:35 -0600 Subject: [PATCH 27/55] Only update m.accepted_terms if there were changes Fixes https://github.com/vector-im/riot-web/issues/10744 --- src/Terms.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Terms.js b/src/Terms.js index 594f15b522..a664d3f72e 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -116,6 +116,7 @@ export async function startTermsFlow( } // if there's anything left to agree to, prompt the user + let numAcceptedBeforeAgreement = agreedUrlSet.size; if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); @@ -125,8 +126,11 @@ export async function startTermsFlow( console.log("User has already agreed to all required policies"); } - const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) }; - await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + // We only ever add to the set of URLs, so if anything has changed then we'd see a different length + if (agreedUrlSet.size !== numAcceptedBeforeAgreement) { + const newAcceptedTerms = {accepted: Array.from(agreedUrlSet)}; + await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + } const agreePromises = policiesAndServicePairs.map((policiesAndService) => { // filter the agreed URL list for ones that are actually for this service From b35d56167e398ecc834df78d26f2ab580520f9f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:16:45 -0600 Subject: [PATCH 28/55] const-antly annoying linter although it's a valid complaint here --- src/Terms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terms.js b/src/Terms.js index a664d3f72e..685a39709c 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -116,7 +116,7 @@ export async function startTermsFlow( } // if there's anything left to agree to, prompt the user - let numAcceptedBeforeAgreement = agreedUrlSet.size; + const numAcceptedBeforeAgreement = agreedUrlSet.size; if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); From ad2e16d432454a470cab42827f708153ae3f7299 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 13:46:18 +0200 Subject: [PATCH 29/55] keep filter field expanded if it has text in it --- src/components/structures/LeftPanel.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 4c5b1686f5..fd315d2540 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -81,7 +81,7 @@ const LeftPanel = createReactClass({ if (this.state.searchFilter !== nextState.searchFilter) { return true; } - if (this.state.searchFocused !== nextState.searchFocused) { + if (this.state.searchExpanded !== nextState.searchExpanded) { return true; } @@ -206,6 +206,7 @@ const LeftPanel = createReactClass({ if (source === "keyboard") { dis.dispatch({action: 'focus_composer'}); } + this.setState({searchExpanded: false}); }, collectRoomList: function(ref) { @@ -213,11 +214,13 @@ const LeftPanel = createReactClass({ }, _onSearchFocus: function() { - this.setState({searchFocused: true}); + this.setState({searchExpanded: true}); }, - _onSearchBlur: function() { - this.setState({searchFocused: false}); + _onSearchBlur: function(event) { + if (event.target.value.length === 0) { + this.setState({searchExpanded: false}); + } }, render: function() { @@ -255,7 +258,7 @@ const LeftPanel = createReactClass({ let exploreButton; if (!this.props.collapsed) { exploreButton = ( -
+
dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}
); From 7754e08d84005a1f7fe4544c5bdd665b304dcd2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:07:01 +0200 Subject: [PATCH 30/55] remove labels and add join/view & preview button instead in directory --- src/components/structures/RoomDirectory.js | 159 ++++++++++++--------- src/i18n/strings/en_EN.json | 2 + 2 files changed, 93 insertions(+), 68 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 13ae83b067..55078e68be 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -321,7 +321,7 @@ module.exports = createReactClass({ } }, - onJoinClick: function(alias) { + onJoinFromSearchClick: function(alias) { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId) { // If the user specified an alias without a domain, add on whichever server is selected @@ -363,6 +363,34 @@ module.exports = createReactClass({ } }, + onPreviewClick: function(room) { + this.props.onFinished(); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + should_peek: true, + }); + }, + + onViewClick: function(room) { + this.props.onFinished(); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + should_peek: false, + }); + }, + + onJoinClick: function(room) { + this.props.onFinished(); + MatrixClientPeg.get().joinRoom(room.room_id); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + joining: true, + }); + }, + showRoomAlias: function(alias, autoJoin=false) { this.showRoom(null, alias, autoJoin); }, @@ -407,74 +435,69 @@ module.exports = createReactClass({ dis.dispatch(payload); }, - getRows: function() { + getRow(room) { + const client = MatrixClientPeg.get(); + const clientRoom = client.getRoom(room.room_id); + const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join"; + const isGuest = client.isGuest(); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let previewButton; + let joinOrViewButton; - if (!this.state.publicRooms) return []; - - const rooms = this.state.publicRooms; - const rows = []; - const self = this; - let guestRead; let guestJoin; let perms; - for (let i = 0; i < rooms.length; i++) { - guestRead = null; - guestJoin = null; - - if (rooms[i].world_readable) { - guestRead = ( -
{ _t('World readable') }
- ); - } - if (rooms[i].guest_can_join) { - guestJoin = ( -
{ _t('Guests can join') }
- ); - } - - perms = null; - if (guestRead || guestJoin) { - perms =
{guestRead}{guestJoin}
; - } - - let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room'); - if (name.length > MAX_NAME_LENGTH) { - name = `${name.substring(0, MAX_NAME_LENGTH)}...`; - } - - let topic = rooms[i].topic || ''; - if (topic.length > MAX_TOPIC_LENGTH) { - topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; - } - topic = linkifyAndSanitizeHtml(topic); - - rows.push( - {ev.preventDefault();}} - > - - - - -
{ name }
  - { perms } -
-
{ get_display_alias_for_room(rooms[i]) }
- - - { rooms[i].num_joined_members } - - , + if (room.world_readable && !hasJoinedRoom) { + previewButton = ( + this.onPreviewClick(room)}>{_t("Preview")} ); } - return rows; + if (hasJoinedRoom) { + joinOrViewButton = ( + this.onViewClick(room)}>{_t("View")} + ); + } else if (!isGuest || room.guest_can_join) { + joinOrViewButton = ( + this.onJoinClick(room)}>{_t("Join")} + ); + } + + let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room'); + if (name.length > MAX_NAME_LENGTH) { + name = `${name.substring(0, MAX_NAME_LENGTH)}...`; + } + + let topic = room.topic || ''; + if (topic.length > MAX_TOPIC_LENGTH) { + topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; + } + topic = linkifyAndSanitizeHtml(topic); + + return ( + this.onRoomClicked(room)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + > + + + + +
{ name }
  +
+
{ get_display_alias_for_room(room) }
+ + + { room.num_joined_members } + + {previewButton} + {joinOrViewButton} + + ); }, collectScrollPanel: function(element) { @@ -528,7 +551,7 @@ module.exports = createReactClass({ } else if (this.state.protocolsLoading || this.state.loading) { content = ; } else { - const rows = this.getRows(); + const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one @@ -538,7 +561,7 @@ module.exports = createReactClass({ } else { scrollpanel_content = - { this.getRows() } + { rows }
; } @@ -590,7 +613,7 @@ module.exports = createReactClass({ listHeader =
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c7ce3cd19b..ebbe055694 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1598,6 +1598,8 @@ "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", "Fetching third party location failed": "Fetching third party location failed", "Unable to look up room ID from server": "Unable to look up room ID from server", + "Preview": "Preview", + "View": "View", "Search for a room": "Search for a room", "Search for a room like #example": "Search for a room like #example", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", From 6f62cdb22cb833aee3cfce32532d8413e3fae08f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:07:27 +0200 Subject: [PATCH 31/55] basic styling to make it look ok before applying new design --- res/css/structures/_RoomDirectory.scss | 9 +++++++++ res/css/views/elements/_AccessibleButton.scss | 1 + 2 files changed, 10 insertions(+) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 1df0a61a2b..6989c3b0b0 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -135,4 +135,13 @@ limitations under the License. .mx_RoomDirectory_table tr { padding-bottom: 10px; cursor: pointer; + + .mx_RoomDirectory_roomDescription { + width: 50%; + } + + .mx_RoomDirectory_join, .mx_RoomDirectory_preview { + width: 80px; + text-align: center; + } } diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0c081ec0d5..5ca5d002ba 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_AccessibleButton { cursor: pointer; + white-space: nowrap; } .mx_AccessibleButton:focus { From 094c9e346818d4ed1b81cbea9a427555d5b7574a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 14:15:46 +0000 Subject: [PATCH 32/55] consistent naming Co-Authored-By: Travis Ralston --- src/components/structures/RoomDirectory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 55078e68be..d6607461cf 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -487,7 +487,7 @@ module.exports = createReactClass({
{ name }
 
{ ev.stopPropagation(); } } dangerouslySetInnerHTML={{ __html: topic }} />
{ get_display_alias_for_room(room) }
From 77175fcbc4a1d40a3a38bbcc8d1e1c80a8539c79 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 16:18:34 +0200 Subject: [PATCH 33/55] pr feedback --- src/components/structures/RoomDirectory.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index d6607461cf..c4f7ffe82e 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -470,7 +470,10 @@ module.exports = createReactClass({ topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } topic = linkifyAndSanitizeHtml(topic); - + const avatarUrl = ContentRepo.getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), + room.avatar_url, 24, 24, "crop", + ); return ( this.onRoomClicked(room)} @@ -480,15 +483,13 @@ module.exports = createReactClass({ + url={ avatarUrl } />
{ name }
 
{ ev.stopPropagation(); } } - dangerouslySetInnerHTML={{ __html: topic }} /> + onClick={ (ev) => { ev.stopPropagation(); } } + dangerouslySetInnerHTML={{ __html: topic }} />
{ get_display_alias_for_room(room) }
From 24c4a16da177883928e11fea92efe4cf1053f81b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 15:36:29 +0200 Subject: [PATCH 34/55] adapt design of room list in directory --- res/css/structures/_RoomDirectory.scss | 82 +++++++++++----------- src/components/structures/RoomDirectory.js | 26 +++++-- src/i18n/strings/en_EN.json | 6 +- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 6989c3b0b0..6b7a4ff0c7 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -17,7 +17,6 @@ limitations under the License. .mx_RoomDirectory_dialogWrapper > .mx_Dialog { max-width: 960px; height: 100%; - padding: 20px; } .mx_RoomDirectory_dialog { @@ -35,17 +34,6 @@ limitations under the License. flex: 1; } -.mx_RoomDirectory_createRoom { - background-color: $button-bg-color; - border-radius: 4px; - padding: 8px; - color: $button-fg-color; - font-weight: 600; - position: absolute; - top: 0; - left: 0; -} - .mx_RoomDirectory_list { flex: 1; display: flex; @@ -84,9 +72,8 @@ limitations under the License. } .mx_RoomDirectory_roomAvatar { - width: 24px; - padding-left: 12px; - padding-right: 24px; + width: 32px; + padding-right: 14px; vertical-align: top; } @@ -94,6 +81,34 @@ limitations under the License. padding-bottom: 16px; } +.mx_RoomDirectory_roomMemberCount { + color: $light-fg-color; + width: 60px; + padding: 0 10px; + text-align: center; + + &::before { + background-color: $light-fg-color; + display: inline-block; + vertical-align: text-top; + margin-right: 2px; + content: ""; + mask: url('$(res)/img/feather-customised/user.svg'); + mask-repeat: no-repeat; + mask-position: center; + // scale it down and make the size slightly bigger (16 instead of 14px) + // to avoid rendering artifacts + mask-size: 80%; + width: 16px; + height: 16px; + } +} + +.mx_RoomDirectory_join, .mx_RoomDirectory_preview { + width: 80px; + text-align: center; +} + .mx_RoomDirectory_name { display: inline-block; font-weight: 600; @@ -103,22 +118,9 @@ limitations under the License. display: inline-block; } -.mx_RoomDirectory_perm { - display: inline; - padding-left: 5px; - padding-right: 5px; - margin-right: 5px; - height: 15px; - border-radius: 11px; - background-color: $plinth-bg-color; - text-transform: uppercase; - font-weight: 600; - font-size: 11px; - color: $accent-color; -} - .mx_RoomDirectory_topic { cursor: initial; + color: $light-fg-color; } .mx_RoomDirectory_alias { @@ -126,22 +128,20 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_roomMemberCount { - text-align: right; - width: 100px; - padding-right: 10px; -} - .mx_RoomDirectory_table tr { padding-bottom: 10px; cursor: pointer; +} - .mx_RoomDirectory_roomDescription { - width: 50%; - } +.mx_RoomDirectory .mx_RoomView_MessageList { + padding: 0; +} - .mx_RoomDirectory_join, .mx_RoomDirectory_preview { - width: 80px; - text-align: center; +.mx_RoomDirectory p { + font-size: 14px; + margin-top: 0; + + .mx_AccessibleButton { + padding: 0; } } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index c4f7ffe82e..b85dc20b21 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -391,6 +391,11 @@ module.exports = createReactClass({ }); }, + onCreateRoomClick: function(room) { + this.props.onFinished(); + dis.dispatch({action: 'view_create_room'}); + }, + showRoomAlias: function(alias, autoJoin=false) { this.showRoom(null, alias, autoJoin); }, @@ -472,7 +477,7 @@ module.exports = createReactClass({ topic = linkifyAndSanitizeHtml(topic); const avatarUrl = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), - room.avatar_url, 24, 24, "crop", + room.avatar_url, 32, 32, "crop", ); return ( {ev.preventDefault();}} > - @@ -595,10 +600,9 @@ module.exports = createReactClass({ instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; } - - let placeholder = _t('Search for a room'); + let placeholder = _t('Find a room…'); if (!this.state.instanceId) { - placeholder = _t('Search for a room like #example') + ':' + this.state.roomServer; + placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer}); } else if (instance_expected_field_type) { placeholder = instance_expected_field_type.placeholder; } @@ -620,15 +624,25 @@ module.exports = createReactClass({
; } + const explanation = + _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, + {a: sub => { + return ({sub}); + }}, + ); return (
+

{explanation}

{listHeader} {content} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ebbe055694..108122d7c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1600,8 +1600,10 @@ "Unable to look up room ID from server": "Unable to look up room ID from server", "Preview": "Preview", "View": "View", - "Search for a room": "Search for a room", - "Search for a room like #example": "Search for a room like #example", + "Find a room…": "Find a room…", + "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", + "Explore rooms": "Explore rooms", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Show devices, send anyway or cancel.": "Show devices, send anyway or cancel.", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", From 886402fe319a2d0a9003436552accabbf3963404 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 15:36:59 +0200 Subject: [PATCH 35/55] reduce padding on dialogs, as in design --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 77b474a013..2b627cce9f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -330,7 +330,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_header { position: relative; - margin-bottom: 20px; + margin-bottom: 10px; } .mx_Dialog_title { From f04c347df7dad67cc698ca246f12188bfde71803 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 11:33:58 +0100 Subject: [PATCH 36/55] Lift 3PID state management up to Settings tab This pulls the 3PID state management in Settings up to a single location so that the Account and Discovery sections now work from a single list that updates immediately. Fixes https://github.com/vector-im/riot-web/issues/10519 --- src/boundThreepids.js | 5 +-- src/components/views/settings/SetIdServer.js | 4 +-- .../views/settings/account/EmailAddresses.js | 33 ++++++++--------- .../views/settings/account/PhoneNumbers.js | 33 ++++++++--------- .../settings/discovery/EmailAddresses.js | 22 +++--------- .../views/settings/discovery/PhoneNumbers.js | 22 +++--------- .../tabs/user/GeneralUserSettingsTab.js | 35 ++++++++++++++++--- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 799728f801..90a5bd2a6f 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -16,7 +16,7 @@ limitations under the License. import IdentityAuthClient from './IdentityAuthClient'; -export async function getThreepidBindStatus(client, filterMedium) { +export async function getThreepidsWithBindStatus(client, filterMedium) { const userId = client.getUserId(); let { threepids } = await client.getThreePids(); @@ -24,7 +24,8 @@ export async function getThreepidBindStatus(client, filterMedium) { threepids = threepids.filter((a) => a.medium === filterMedium); } - if (threepids.length > 0) { + // Check bind status assuming we have an IS and terms are agreed + if (threepids.length > 0 && client.getIdentityServerUrl()) { // TODO: Handle terms agreement // See https://github.com/vector-im/riot-web/issues/10522 const authClient = new IdentityAuthClient(); diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ae550725f1..9ef5fb295e 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -22,7 +22,7 @@ import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; import dis from "../../../dispatcher"; -import { getThreepidBindStatus } from '../../../boundThreepids'; +import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {SERVICE_TYPES} from "matrix-js-sdk"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; @@ -249,7 +249,7 @@ export default class SetIdServer extends React.Component { }; async _showServerChangeWarning({ title, unboundMessage, button }) { - const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); + const threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get()); const boundThreepids = threepids.filter(tp => tp.bound); let message; diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index eb60d4a322..b7324eb272 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -23,8 +23,8 @@ import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; import * as Email from "../../../../email"; import AddThreepid from "../../../../AddThreepid"; -const sdk = require('../../../../index'); -const Modal = require("../../../../Modal"); +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; /* TODO: Improve the UX for everything in here. @@ -113,11 +113,15 @@ export class ExistingEmailAddress extends React.Component { } export default class EmailAddresses extends React.Component { - constructor() { - super(); + static propTypes = { + emails: PropTypes.array.isRequired, + onEmailsChange: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); this.state = { - emails: [], verifying: false, addTask: null, continueDisabled: false, @@ -125,16 +129,9 @@ export default class EmailAddresses extends React.Component { }; } - componentWillMount(): void { - const client = MatrixClientPeg.get(); - - client.getThreePids().then((addresses) => { - this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')}); - }); - } - _onRemoved = (address) => { - this.setState({emails: this.state.emails.filter((e) => e !== address)}); + const emails = this.props.emails.filter((e) => e !== address); + this.props.onEmailsChange(emails); }; _onChangeNewEmailAddress = (e) => { @@ -184,12 +181,16 @@ export default class EmailAddresses extends React.Component { this.state.addTask.checkEmailLinkClicked().then(() => { const email = this.state.newEmailAddress; this.setState({ - emails: [...this.state.emails, {address: email, medium: "email"}], addTask: null, continueDisabled: false, verifying: false, newEmailAddress: "", }); + const emails = [ + ...this.props.emails, + { address: email, medium: "email" }, + ]; + this.props.onEmailsChange(emails); }).catch((err) => { this.setState({continueDisabled: false}); if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { @@ -204,7 +205,7 @@ export default class EmailAddresses extends React.Component { }; render() { - const existingEmailElements = this.state.emails.map((e) => { + const existingEmailElements = this.props.emails.map((e) => { return ; }); diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index fbb5b7e561..8f91eb22cc 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -23,8 +23,8 @@ import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; import AddThreepid from "../../../../AddThreepid"; import CountryDropdown from "../../auth/CountryDropdown"; -const sdk = require('../../../../index'); -const Modal = require("../../../../Modal"); +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; /* TODO: Improve the UX for everything in here. @@ -108,11 +108,15 @@ export class ExistingPhoneNumber extends React.Component { } export default class PhoneNumbers extends React.Component { - constructor() { - super(); + static propTypes = { + msisdns: PropTypes.array.isRequired, + onMsisdnsChange: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); this.state = { - msisdns: [], verifying: false, verifyError: false, verifyMsisdn: "", @@ -124,16 +128,9 @@ export default class PhoneNumbers extends React.Component { }; } - componentWillMount(): void { - const client = MatrixClientPeg.get(); - - client.getThreePids().then((addresses) => { - this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')}); - }); - } - _onRemoved = (address) => { - this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)}); + const msisdns = this.props.msisdns.filter((e) => e !== address); + this.props.onMsisdnsChange(msisdns); }; _onChangeNewPhoneNumber = (e) => { @@ -181,7 +178,6 @@ export default class PhoneNumbers extends React.Component { const token = this.state.newPhoneNumberCode; this.state.addTask.haveMsisdnToken(token).then(() => { this.setState({ - msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}], addTask: null, continueDisabled: false, verifying: false, @@ -190,6 +186,11 @@ export default class PhoneNumbers extends React.Component { newPhoneNumber: "", newPhoneNumberCode: "", }); + const msisdns = [ + ...this.props.msisdns, + { address: this.state.verifyMsisdn, medium: "msisdn" }, + ]; + this.props.onMsisdnsChange(msisdns); }).catch((err) => { this.setState({continueDisabled: false}); if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { @@ -210,7 +211,7 @@ export default class PhoneNumbers extends React.Component { }; render() { - const existingPhoneElements = this.state.msisdns.map((p) => { + const existingPhoneElements = this.props.msisdns.map((p) => { return ; }); diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index 4d18c1d355..c6ec826de6 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; import AddThreepid from '../../../../AddThreepid'; -import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -187,27 +186,14 @@ export class EmailAddress extends React.Component { } export default class EmailAddresses extends React.Component { - constructor() { - super(); - - this.state = { - loaded: false, - emails: [], - }; - } - - async componentWillMount() { - const client = MatrixClientPeg.get(); - - const emails = await getThreepidBindStatus(client, 'email'); - - this.setState({ emails }); + static propTypes = { + emails: PropTypes.array.isRequired, } render() { let content; - if (this.state.emails.length > 0) { - content = this.state.emails.map((e) => { + if (this.props.emails.length > 0) { + content = this.props.emails.map((e) => { return ; }); } else { diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index fdebac5d22..6d5c8ad3b4 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; import AddThreepid from '../../../../AddThreepid'; -import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -206,27 +205,14 @@ export class PhoneNumber extends React.Component { } export default class PhoneNumbers extends React.Component { - constructor() { - super(); - - this.state = { - loaded: false, - msisdns: [], - }; - } - - async componentWillMount() { - const client = MatrixClientPeg.get(); - - const msisdns = await getThreepidBindStatus(client, 'msisdn'); - - this.setState({ msisdns }); + static propTypes = { + msisdns: PropTypes.array.isRequired, } render() { let content; - if (this.state.msisdns.length > 0) { - content = this.state.msisdns.map((e) => { + if (this.props.msisdns.length > 0) { + content = this.props.msisdns.map((e) => { return ; }); } else { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 9c37730fc5..02f9cf3cd3 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -37,6 +37,7 @@ import {Service, startTermsFlow} from "../../../../../Terms"; import {SERVICE_TYPES} from "matrix-js-sdk"; import IdentityAuthClient from "../../../../../IdentityAuthClient"; import {abbreviateUrl} from "../../../../../utils/UrlUtils"; +import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -58,17 +59,27 @@ export default class GeneralUserSettingsTab extends React.Component { // agreedUrls, // From the startTermsFlow callback // resolve, // Promise resolve function for startTermsFlow callback }, + emails: [], + msisdns: [], }; this.dispatcherRef = dis.register(this._onAction); } async componentWillMount() { - const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam(); + const cli = MatrixClientPeg.get(); + + const serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); // Check to see if terms need accepting this._checkTerms(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + const threepids = await getThreepidsWithBindStatus(cli); + this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); + this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); } componentWillUnmount() { @@ -82,6 +93,14 @@ export default class GeneralUserSettingsTab extends React.Component { } }; + _onEmailsChange = (emails) => { + this.setState({ emails }); + } + + _onMsisdnsChange = (msisdns) => { + this.setState({ msisdns }); + } + async _checkTerms() { if (!this.state.haveIdServer) { this.setState({idServerHasUnsignedTerms: false}); @@ -200,10 +219,16 @@ export default class GeneralUserSettingsTab extends React.Component { if (this.state.haveIdServer || this.state.serverRequiresIdServer === false) { threepidSection =
{_t("Email addresses")} - + {_t("Phone numbers")} - +
; } else if (this.state.serverRequiresIdServer === null) { threepidSection = ; @@ -279,10 +304,10 @@ export default class GeneralUserSettingsTab extends React.Component { const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} - + {_t("Phone numbers")} - +
: null; return ( From 0b7995dc11f06084540037201e3fad16214c26a0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 13:36:10 +0100 Subject: [PATCH 37/55] Improve terms handling for 3PID state gathering This changes the 3PID state gathering (used in Settings) to ignore terms errors (no modals will be shown) on the assumption that other UX handles this case. --- src/IdentityAuthClient.js | 2 +- src/boundThreepids.js | 37 +++++++++++-------- .../tabs/user/GeneralUserSettingsTab.js | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 075ae93709..7cbad074bf 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -65,7 +65,7 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - async getAccessToken(check=true) { + async getAccessToken({ check = true } = {}) { if (!this.authEnabled) { // The current IS doesn't support authentication return null; diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 90a5bd2a6f..7f727d8e64 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -26,26 +26,31 @@ export async function getThreepidsWithBindStatus(client, filterMedium) { // Check bind status assuming we have an IS and terms are agreed if (threepids.length > 0 && client.getIdentityServerUrl()) { - // TODO: Handle terms agreement - // See https://github.com/vector-im/riot-web/issues/10522 - const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); + try { + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken({ check: false }); - // Restructure for lookup query - const query = threepids.map(({ medium, address }) => [medium, address]); - const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + // Restructure for lookup query + const query = threepids.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); - // Record which are already bound - for (const [medium, address, mxid] of lookupResults.threepids) { - if (mxid !== userId) { - continue; + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (mxid !== userId) { + continue; + } + if (filterMedium && medium !== filterMedium) { + continue; + } + const threepid = threepids.find(e => e.medium === medium && e.address === address); + if (!threepid) continue; + threepid.bound = true; } - if (filterMedium && medium !== filterMedium) { - continue; + } catch (e) { + // Ignore terms errors here and assume other flows handle this + if (!(e.errcode === "M_TERMS_NOT_SIGNED")) { + throw e; } - const threepid = threepids.find(e => e.medium === medium && e.address === address); - if (!threepid) continue; - threepid.bound = true; } } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 02f9cf3cd3..f1ca314f13 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -110,7 +110,7 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. const authClient = new IdentityAuthClient(); - const idAccessToken = await authClient.getAccessToken(/*check=*/false); + const idAccessToken = await authClient.getAccessToken({ check: false }); startTermsFlow([new Service( SERVICE_TYPES.IS, MatrixClientPeg.get().getIdentityServerUrl(), From db33c138aa50d60a4c93e0c3eba08107e3fcf421 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 16:16:07 +0100 Subject: [PATCH 38/55] Update all 3PID state in Settings when IS changes This ensures we update all 3PID state (like bind status) whenever the IS changes. --- .../settings/discovery/EmailAddresses.js | 5 ++++ .../views/settings/discovery/PhoneNumbers.js | 5 ++++ .../tabs/user/GeneralUserSettingsTab.js | 24 ++++++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index c6ec826de6..d6628f900a 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -58,6 +58,11 @@ export class EmailAddress extends React.Component { }; } + componentWillReceiveProps(nextProps) { + const { bound } = nextProps.email; + this.setState({ bound }); + } + async changeBinding({ bind, label, errorTitle }) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const { medium, address } = this.props.email; diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index 6d5c8ad3b4..99a90f23fb 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -50,6 +50,11 @@ export class PhoneNumber extends React.Component { }; } + componentWillReceiveProps(nextProps) { + const { bound } = nextProps.msisdn; + this.setState({ bound }); + } + async changeBinding({ bind, label, errorTitle }) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const { medium, address } = this.props.msisdn; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index f1ca314f13..b378db707a 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -72,14 +72,7 @@ export default class GeneralUserSettingsTab extends React.Component { const serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); - // Check to see if terms need accepting - this._checkTerms(); - - // Need to get 3PIDs generally for Account section and possibly also for - // Discovery (assuming we have an IS and terms are agreed). - const threepids = await getThreepidsWithBindStatus(cli); - this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); - this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); + this._getThreepidState(); } componentWillUnmount() { @@ -89,7 +82,7 @@ export default class GeneralUserSettingsTab extends React.Component { _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); - this._checkTerms(); + this._getThreepidState(); } }; @@ -101,6 +94,19 @@ export default class GeneralUserSettingsTab extends React.Component { this.setState({ msisdns }); } + async _getThreepidState() { + const cli = MatrixClientPeg.get(); + + // Check to see if terms need accepting + this._checkTerms(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + const threepids = await getThreepidsWithBindStatus(cli); + this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); + this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); + } + async _checkTerms() { if (!this.state.haveIdServer) { this.setState({idServerHasUnsignedTerms: false}); From c542ea555967f1f8a8bfcb4e4ffaf7c30b563a95 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 16:55:45 +0100 Subject: [PATCH 39/55] Force boolean value --- src/boundThreepids.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 7f727d8e64..3b32815913 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -25,7 +25,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) { } // Check bind status assuming we have an IS and terms are agreed - if (threepids.length > 0 && client.getIdentityServerUrl()) { + if (threepids.length > 0 && !!client.getIdentityServerUrl()) { try { const authClient = new IdentityAuthClient(); const identityAccessToken = await authClient.getAccessToken({ check: false }); From 0d28cd58404a80a828c2841c5aedf3af4eaef0bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:52:39 +0100 Subject: [PATCH 40/55] RoomDirectory: show spinner if loading more results Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomDirectory.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index b85dc20b21..299216d022 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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. @@ -140,6 +141,10 @@ module.exports = createReactClass({ getMoreRooms: function() { if (!MatrixClientPeg.get()) return Promise.resolve(); + this.setState({ + loading: true, + }); + const my_filter_string = this.state.filterString; const my_server = this.state.roomServer; // remember the next batch token when we sent the request @@ -554,15 +559,21 @@ module.exports = createReactClass({ let content; if (this.state.error) { content = this.state.error; - } else if (this.state.protocolsLoading || this.state.loading) { + } else if (this.state.protocolsLoading) { content = ; } else { const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one + + let spinner; + if (this.state.loading) { + spinner = ; + } + let scrollpanel_content; - if (rows.length == 0) { + if (rows.length === 0 && !this.state.loading) { scrollpanel_content = { _t('No rooms to show') }; } else { scrollpanel_content = @@ -579,6 +590,7 @@ module.exports = createReactClass({ startAtBottom={false} > { scrollpanel_content } + { spinner } ; } From 980e9839ac8ded7187191500db4062c7d4791fa8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 16:02:37 +0100 Subject: [PATCH 41/55] Update Copyright --- src/components/structures/RoomDirectory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 299216d022..d3c65dceda 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From e3643bf17abf2f88704aa0807ece25728e2e76e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 18:12:52 +0100 Subject: [PATCH 42/55] EditMessageComposer: disable Save button until a change has been made Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/EditMessageComposer.js | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index d58279436d..58f1f75726 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -112,6 +112,10 @@ export default class EditMessageComposer extends React.Component { super(props, context); this.model = null; this._editorRef = null; + + this.state = { + changed: false, + }; } _setEditorRef = ref => { @@ -176,16 +180,16 @@ export default class EditMessageComposer extends React.Component { const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); const newContent = editContent["m.new_content"]; - if (!this._isModifiedOrSameAsOld(newContent)) { - return; + + if (this._isModifiedOrSameAsOld(newContent)) { + const roomId = editedEvent.getRoomId(); + this._cancelPreviousPendingEdit(); + this.context.matrixClient.sendMessage(roomId, editContent); } - const roomId = editedEvent.getRoomId(); - this._cancelPreviousPendingEdit(); - this.context.matrixClient.sendMessage(roomId, editContent); dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: 'focus_composer'}); - } + }; _cancelPreviousPendingEdit() { const originalEvent = this.props.editState.getEvent(); @@ -240,6 +244,16 @@ export default class EditMessageComposer extends React.Component { return caretPosition; } + _onChange = () => { + if (this.state.changed || !this._editorRef || !this._editorRef.isModified()) { + return; + } + + this.setState({ + changed: true, + }); + }; + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
@@ -249,10 +263,13 @@ export default class EditMessageComposer extends React.Component { room={this._getRoom()} initialCaret={this.props.editState.getCaret()} label={_t("Edit message")} + onChange={this._onChange} />
{_t("Cancel")} - {_t("Save")} + + {_t("Save")} +
); } From 2ff98b7c1f99ef2b9ec09c878c1824b0791f9f83 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:51:27 +0100 Subject: [PATCH 43/55] clear up ambiguity by poor naming Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EditMessageComposer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 58f1f75726..89aea64139 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -164,7 +164,7 @@ export default class EditMessageComposer extends React.Component { dis.dispatch({action: 'focus_composer'}); } - _isModifiedOrSameAsOld(newContent) { + _isContentModified(newContent) { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); if (!this._editorRef.isModified() || @@ -181,12 +181,14 @@ export default class EditMessageComposer extends React.Component { const editContent = createEditContent(this.model, editedEvent); const newContent = editContent["m.new_content"]; - if (this._isModifiedOrSameAsOld(newContent)) { + // If content is modified then send an updated event into the room + if (this._isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); this._cancelPreviousPendingEdit(); this.context.matrixClient.sendMessage(roomId, editContent); } + // close the event editing and focus composer dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: 'focus_composer'}); }; From 792b70913c5d43ef3ea3b5bf17e4a2d323f09d31 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 17:29:52 +0100 Subject: [PATCH 44/55] invert and rename changed to saveDisabled for clarity Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EditMessageComposer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 89aea64139..a1d6fa618f 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -114,7 +114,7 @@ export default class EditMessageComposer extends React.Component { this._editorRef = null; this.state = { - changed: false, + saveDisabled: true, }; } @@ -247,12 +247,12 @@ export default class EditMessageComposer extends React.Component { } _onChange = () => { - if (this.state.changed || !this._editorRef || !this._editorRef.isModified()) { + if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) { return; } this.setState({ - changed: true, + saveDisabled: false, }); }; @@ -269,7 +269,7 @@ export default class EditMessageComposer extends React.Component { />
{_t("Cancel")} - + {_t("Save")}
From f1ea5ff6f39d831e61bb25dadf0889b55e572dae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 10:49:58 +0100 Subject: [PATCH 45/55] Login: don't assume supported flows, prevent login flash on SSO servers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 014fb4426d..ab76c990b6 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -93,7 +93,7 @@ module.exports = createReactClass({ // Phase of the overall login dialog. phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. - currentFlow: "m.login.password", + currentFlow: null, // we need to load the flows from the server // We perform liveliness checks later, but for now suppress the errors. // We also track the server dead errors independently of the regular errors so @@ -372,6 +372,7 @@ module.exports = createReactClass({ this.setState({ busy: true, + currentFlow: null, // reset flow loginIncorrect: false, }); From 1c7d67e8b3ff652b2220e6867f2f72069557df47 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:01:06 +0100 Subject: [PATCH 46/55] fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/auth/Login-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index 74451b922f..e79cf037d0 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -75,6 +75,11 @@ describe('Login', function() { const root = render(); + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + currentFlow: "m.login.password", + }); + const form = ReactTestUtils.findRenderedComponentWithType( root, sdk.getComponent('auth.PasswordLogin'), From bf30cfe6995799b997c705b2ce8bb49e015ffb11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:20:03 +0100 Subject: [PATCH 47/55] Fix other test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/auth/Login-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index e79cf037d0..6a7982dd47 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -55,6 +55,11 @@ describe('Login', function() { it('should show form with change server link', function() { const root = render(); + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + currentFlow: "m.login.password", + }); + const form = ReactTestUtils.findRenderedComponentWithType( root, sdk.getComponent('auth.PasswordLogin'), From 76e4363452f4d175b55b28f850d85bdfefd95624 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 12:20:36 +0100 Subject: [PATCH 48/55] Login: Add way to change HS from SSO Homeserver Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 8 ++- src/components/views/auth/PasswordLogin.js | 35 ++---------- src/components/views/auth/SignInToText.js | 62 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 src/components/views/auth/SignInToText.js diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ab76c990b6..594a484bde 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -566,6 +566,7 @@ module.exports = createReactClass({ }, _renderSsoStep: function(url) { + const SignInToText = sdk.getComponent('views.auth.SignInToText'); // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means // we use the same window and redirect back to riot. On electron, this actually @@ -574,9 +575,12 @@ module.exports = createReactClass({ // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser // to vector://vector which, of course, will not work. - return ( + return ; }, render: function() { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 59acf0a034..63e77a938d 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -31,6 +31,7 @@ export default class PasswordLogin extends React.Component { static propTypes = { onSubmit: PropTypes.func.isRequired, // fn(username, password) onError: PropTypes.func, + onEditServerDetailsClick: PropTypes.func, onForgotPasswordClick: PropTypes.func, // fn() initialUsername: PropTypes.string, initialPhoneCountry: PropTypes.string, @@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component { render() { const Field = sdk.getComponent('elements.Field'); + const SignInToText = sdk.getComponent('views.auth.SignInToText'); let forgotPasswordJsx; @@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component { ; } - let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - signInToText = _t('Sign in to your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - let editLink = null; - if (this.props.onEditServerDetailsClick) { - editLink = - {_t('Change')} - ; - } - const pwFieldClass = classNames({ error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field }); @@ -342,10 +317,8 @@ export default class PasswordLogin extends React.Component { return (
-

- {signInToText} - {editLink} -

+
{loginType} {loginField} diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js new file mode 100644 index 0000000000..edbe2fd661 --- /dev/null +++ b/src/components/views/auth/SignInToText.js @@ -0,0 +1,62 @@ +/* +Copyright 2019 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 React from 'react'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import PropTypes from "prop-types"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; + +export default class SignInToText extends React.PureComponent { + static propTypes = { + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + onEditServerDetailsClick: PropTypes.func, + }; + + render() { + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, + }); + } + + let editLink = null; + if (this.props.onEditServerDetailsClick) { + editLink = + {_t('Change')} + ; + } + + return

+ {signInToText} + {editLink} +

; + } +} From 370d9d83364991a710574bbdcc2f5db47c0749b3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 13:57:19 +0100 Subject: [PATCH 49/55] regen 18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 108122d7c9..2c38f918a0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1471,8 +1471,6 @@ "Username": "Username", "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", - "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Sign in with": "Sign in with", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.", @@ -1508,6 +1506,8 @@ "Premium": "Premium", "Premium hosting for organisations Learn more": "Premium hosting for organisations Learn more", "Find other public servers or use a custom server": "Find other public servers or use a custom server", + "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", + "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Please install Chrome, Firefox, or Safari for the best experience.": "Please install Chrome, Firefox, or Safari for the best experience.", From 6f736e84079d8d3bdbb8e19bb89d2e021d0d4ed1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 17:34:29 +0100 Subject: [PATCH 50/55] Apply PR feedback Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 12 +++++++----- src/components/views/auth/SignInToText.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 594a484bde..0317462ebd 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -575,12 +575,14 @@ module.exports = createReactClass({ // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser // to vector://vector which, of course, will not work. - return + ); }, render: function() { diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js index edbe2fd661..a7acdc6705 100644 --- a/src/components/views/auth/SignInToText.js +++ b/src/components/views/auth/SignInToText.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a4a85dc54179cb03554daad1e76678aa8fe99906 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 23:02:52 +0100 Subject: [PATCH 51/55] Hide the change HS url button on SSO login flow if custom urls disabled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 0317462ebd..ad77ed49a5 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -567,6 +567,12 @@ module.exports = createReactClass({ _renderSsoStep: function(url) { const SignInToText = sdk.getComponent('views.auth.SignInToText'); + + let onEditServerDetailsClick = null; + // If custom URLs are allowed, wire up the server details edit link. + if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { + onEditServerDetailsClick = this.onEditServerDetailsClick; + } // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means // we use the same window and redirect back to riot. On electron, this actually @@ -578,7 +584,7 @@ module.exports = createReactClass({ return (
+ onEditServerDetailsClick={onEditServerDetailsClick} /> { _t('Sign in with single sign-on') }
From 4ae4e68967e3d7a8db78b00e581cd773dc8c4de9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:11:32 +0200 Subject: [PATCH 52/55] make explore button and filter field equal width --- res/css/structures/_LeftPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index f83195f847..ac17083621 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -136,7 +136,7 @@ limitations under the License. } .mx_LeftPanel_explore { - flex: 0 0 40%; + flex: 0 0 50%; overflow: hidden; transition: flex-basis 0.2s; box-sizing: border-box; From 7c970787648be9a6004917a116e0c41382ec47b2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:12:06 +0200 Subject: [PATCH 53/55] always show clear button in search box when focused --- src/components/structures/SearchBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 4d68ff4e96..d495fffbc9 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -126,7 +126,7 @@ module.exports = createReactClass({ if (this.props.collapsed) { return null; } - const clearButton = this.state.searchTerm.length > 0 ? + const clearButton = !this.state.blurred ? ( {this._clearSearch("button"); } }> From 95060d4e95c9f2eb73e9ca81334d1ee8f47f1181 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:27:20 +0200 Subject: [PATCH 54/55] reduce vertical padding around explore/filter --- res/css/structures/_LeftPanel.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index ac17083621..85fdfa092d 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -132,6 +132,7 @@ limitations under the License. .mx_SearchBox { flex: 1 1 0; min-width: 0; + margin: 4px 9px 1px 9px; } } @@ -147,8 +148,7 @@ limitations under the License. .mx_AccessibleButton { font-size: 14px; - margin: 9px; - margin-right: 0; + margin: 4px 0 1px 9px; padding: 9px; padding-left: 42px; font-weight: 600; From cc67742fa9d41ec50b69e64a2f740420458d0b91 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 12:24:05 +0200 Subject: [PATCH 55/55] undo whitespace setting, accessible button is used in too many places to make this a safe assumption --- res/css/views/elements/_AccessibleButton.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 5ca5d002ba..0c081ec0d5 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -16,7 +16,6 @@ limitations under the License. .mx_AccessibleButton { cursor: pointer; - white-space: nowrap; } .mx_AccessibleButton:focus {