From b0be99a8f09626fc44ddf5b5c74bcbfda3e6cc3e Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 22 May 2020 14:41:25 -0500 Subject: [PATCH 001/106] Show timestamp of redaction on hover Signed-off-by: Aaron Raimist --- src/components/views/messages/RedactedBody.tsx | 8 +++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index 5dada64b52..5f80460d03 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.tsx @@ -19,6 +19,8 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {formatFullDate} from "../../../DateUtils"; +import SettingsStore from "../../../settings/SettingsStore"; interface IProps { mxEvent: MatrixEvent; @@ -36,8 +38,12 @@ const RedactedBody = React.forwardRef(({mxEvent}, ref) => { text = _t("Message deleted by %(name)s", { name: sender ? sender.name : redactedBecauseUserId }); } + const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); + const fullDate = formatFullDate(new Date(unsigned.redacted_because.origin_server_ts), showTwelveHour); + const titleText = _t("Message deleted on %(date)s", { date: fullDate }); + return ( - + { text } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f79d93b98f..a20acd9015 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1352,6 +1352,7 @@ "reacted with %(shortName)s": "reacted with %(shortName)s", "Message deleted": "Message deleted", "Message deleted by %(name)s": "Message deleted by %(name)s", + "Message deleted on %(date)s": "Message deleted on %(date)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", From edb6bbc6c05eeb52a069beaf1845d1ad409941ad Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 15:33:52 +0100 Subject: [PATCH 002/106] Implement font selection --- .../tabs/user/_AppearanceUserSettingsTab.scss | 14 ++++- .../tabs/user/AppearanceUserSettingsTab.tsx | 53 +++++++++++++++++++ src/dispatcher/actions.ts | 10 ++++ .../payloads/UpdateFontSizePayload.ts | 27 ++++++++++ .../payloads/UpdateSystemFontPayload.ts | 32 +++++++++++ src/i18n/strings/en_EN.json | 2 + src/settings/Settings.js | 14 +++++ ...izeController.js => FontSizeController.ts} | 6 ++- .../controllers/SystemFontController.ts | 36 +++++++++++++ .../controllers/UseSystemFontController.ts | 36 +++++++++++++ src/settings/watchers/FontWatcher.ts | 14 ++++- 11 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/dispatcher/payloads/UpdateFontSizePayload.ts create mode 100644 src/dispatcher/payloads/UpdateSystemFontPayload.ts rename src/settings/controllers/{FontSizeController.js => FontSizeController.ts} (80%) create mode 100644 src/settings/controllers/SystemFontController.ts create mode 100644 src/settings/controllers/UseSystemFontController.ts diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 0756e98782..311a6b7c41 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,8 +15,7 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_themeSection .mx_Field, -.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { +.mx_AppearanceUserSettingsTab .mx_Field { @mixin mx_Settings_fullWidthField; } @@ -124,3 +123,14 @@ limitations under the License. .mx_SettingsTab_customFontSizeField { margin-left: calc($font-16px + 10px); } + +.mx_AppearanceUserSettingsTab_Advanced { + .mx_AppearanceUserSettingsTab_AdvancedToggle { + color: $accent-color; + margin-bottom: 16px; + } + + .mx_AppearanceUserSettingsTab_systemFont { + margin-left: calc($font-16px + 10px); + } +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fe575c2819..fa464526d3 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -52,6 +52,9 @@ interface IState extends IThemeState { customThemeUrl: string, customThemeMessage: CustomThemeMessage, useCustomFontSize: boolean, + useSystemFont: boolean, + systemFont: string, + showAdvanced: boolean, } export default class AppearanceUserSettingsTab extends React.Component { @@ -67,6 +70,9 @@ export default class AppearanceUserSettingsTab extends React.Component {_t("Font size")}
@@ -314,6 +323,49 @@ export default class AppearanceUserSettingsTab extends React.Component; } + private renderAdvancedSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const Field = sdk.getComponent("views.elements.Field"); + + const toggle =
this.setState({showAdvanced: !this.state.showAdvanced})} + > + {this.state.showAdvanced ? "Hide advanced" : "Show advanced"} +
; + + let advanced: React.ReactNode; + + if (this.state.showAdvanced) { + advanced =
+ this.setState({useSystemFont: checked})} + /> + { + this.setState({ + systemFont: value.target.value, + }) + + SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value); + }} + tooltipContent="Set the name of a font installed on your system & Riot will attempt to use it." + disabled={!this.state.useSystemFont} + value={this.state.systemFont} + /> +
; + } + return
+ {toggle} + {advanced} +
+ } + render() { return (
@@ -323,6 +375,7 @@ export default class AppearanceUserSettingsTab extends React.Component {this.renderThemeSection()} {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null} + {this.renderAdvancedSection()}
); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5f7ca1293c..a03c731818 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -69,4 +69,14 @@ export enum Action { * Opens the user menu (previously known as the top left menu). No additional payload information required. */ ToggleUserMenu = "toggle_user_menu", + + /** + * Sets the apps root font size. Should be used with UpdateFontSizePayload + */ + UpdateFontSize = "update-font-size", + + /** + * Sets a system font. Should be used with UpdateSystemFontPayload + */ + UpdateSystemFont = "update-system-font", } diff --git a/src/dispatcher/payloads/UpdateFontSizePayload.ts b/src/dispatcher/payloads/UpdateFontSizePayload.ts new file mode 100644 index 0000000000..3eac3e4607 --- /dev/null +++ b/src/dispatcher/payloads/UpdateFontSizePayload.ts @@ -0,0 +1,27 @@ +/* +Copyright 2020 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. +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface UpdateFontSizePayload extends ActionPayload { + action: Action.UpdateFontSize, + + /** + * The font size to set the root to + */ + size: number; +} diff --git a/src/dispatcher/payloads/UpdateSystemFontPayload.ts b/src/dispatcher/payloads/UpdateSystemFontPayload.ts new file mode 100644 index 0000000000..73475e10d5 --- /dev/null +++ b/src/dispatcher/payloads/UpdateSystemFontPayload.ts @@ -0,0 +1,32 @@ +/* +Copyright 2020 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. +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface UpdateSystemFontPayload extends ActionPayload { + action: Action.UpdateSystemFont, + + /** + * Specify whether to use a system font or the stylesheet font + */ + useSystemFont: boolean; + + /** + * The system font to use + */ + font: string; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 396c3f9111..c53b474b13 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -459,6 +459,8 @@ "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", "Match system theme": "Match system theme", + "Use a system font": "Use a system font", + "System font name": "System font name", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fad932fa4b..44440c1722 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -30,6 +30,8 @@ import PushToMatrixClientController from './controllers/PushToMatrixClientContro import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; import FontSizeController from './controllers/FontSizeController'; +import SystemFontController from './controllers/SystemFontController'; +import UseSystemFontController from './controllers/UseSystemFontController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -313,6 +315,18 @@ export const SETTINGS = { default: true, displayName: _td("Match system theme"), }, + "useSystemFont": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + displayName: _td("Use a system font"), + controller: new UseSystemFontController(), + }, + "systemFont": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: "", + displayName: _td("System font name"), + controller: new SystemFontController(), + }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Allow Peer-to-Peer for 1:1 calls'), diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.ts similarity index 80% rename from src/settings/controllers/FontSizeController.js rename to src/settings/controllers/FontSizeController.ts index 3ef01ab99b..6440fd32fe 100644 --- a/src/settings/controllers/FontSizeController.js +++ b/src/settings/controllers/FontSizeController.ts @@ -16,6 +16,8 @@ limitations under the License. import SettingController from "./SettingController"; import dis from "../../dispatcher/dispatcher"; +import { UpdateFontSizePayload } from "../../dispatcher/payloads/UpdateFontSizePayload"; +import { Action } from "../../dispatcher/actions"; export default class FontSizeController extends SettingController { constructor() { @@ -24,8 +26,8 @@ export default class FontSizeController extends SettingController { onChange(level, roomId, newValue) { // Dispatch font size change so that everything open responds to the change. - dis.dispatch({ - action: "update-font-size", + dis.dispatch({ + action: Action.UpdateFontSize, size: newValue, }); } diff --git a/src/settings/controllers/SystemFontController.ts b/src/settings/controllers/SystemFontController.ts new file mode 100644 index 0000000000..4f591efc17 --- /dev/null +++ b/src/settings/controllers/SystemFontController.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 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. +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 SettingController from "./SettingController"; +import SettingsStore from "../SettingsStore"; +import dis from "../../dispatcher/dispatcher"; +import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload"; +import { Action } from "../../dispatcher/actions"; + +export default class SystemFontController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: Action.UpdateSystemFont, + useSystemFont: SettingsStore.getValue("useSystemFont"), + font: newValue, + }); + } +} diff --git a/src/settings/controllers/UseSystemFontController.ts b/src/settings/controllers/UseSystemFontController.ts new file mode 100644 index 0000000000..d598b25962 --- /dev/null +++ b/src/settings/controllers/UseSystemFontController.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 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. +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 SettingController from "./SettingController"; +import SettingsStore from "../SettingsStore"; +import dis from "../../dispatcher/dispatcher"; +import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload"; +import { Action } from "../../dispatcher/actions"; + +export default class UseSystemFontController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: Action.UpdateSystemFont, + useSystemFont: newValue, + font: SettingsStore.getValue("systemFont"), + }); + } +} diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 5527284cd0..cc843edb4d 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -18,6 +18,8 @@ import dis from '../../dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from '../SettingsStore'; import IWatcher from "./Watcher"; import { toPx } from '../../utils/units'; +import { Action } from '../../dispatcher/actions'; +import { UpdateSystemFontPayload } from '../../dispatcher/payloads/UpdateSystemFontPayload'; export class FontWatcher implements IWatcher { public static readonly MIN_SIZE = 8; @@ -33,6 +35,10 @@ export class FontWatcher implements IWatcher { public start() { this.setRootFontSize(SettingsStore.getValue("baseFontSize")); + this.setSystemFont({ + useSystemFont: SettingsStore.getValue("useSystemFont"), + font: SettingsStore.getValue("systemFont"), + }) this.dispatcherRef = dis.register(this.onAction); } @@ -41,8 +47,10 @@ export class FontWatcher implements IWatcher { } private onAction = (payload) => { - if (payload.action === 'update-font-size') { + if (payload.action === Action.UpdateFontSize) { this.setRootFontSize(payload.size); + } else if (payload.action === Action.UpdateSystemFont) { + this.setSystemFont(payload) } }; @@ -54,4 +62,8 @@ export class FontWatcher implements IWatcher { } (document.querySelector(":root")).style.fontSize = toPx(fontSize); }; + + private setSystemFont = ({useSystemFont, font}) => { + document.body.style.fontFamily = useSystemFont ? font : ""; + } } From aee9cd51a069f5553ea2ca7379f009519dde3424 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 16:34:56 +0100 Subject: [PATCH 003/106] Remove shadowed variable --- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fa464526d3..6c70e89e28 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -22,7 +22,6 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import { enumerateThemes } from "../../../../../theme"; import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher"; -import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher/dispatcher"; From 02ccdcb802fecff01d103984f9e1765d3e8eace4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 17:42:30 +0100 Subject: [PATCH 004/106] Fix field width and add tooltip --- .../settings/tabs/user/_AppearanceUserSettingsTab.scss | 7 +++++-- src/components/views/elements/Field.tsx | 8 +++++--- .../settings/tabs/user/AppearanceUserSettingsTab.tsx | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 311a6b7c41..ce99b85d35 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab .mx_Field { +.mx_AppearanceUserSettingsTab_fontSlider { @mixin mx_Settings_fullWidthField; } +.mx_AppearanceUserSettingsTab .mx_Field { + width: 256px; +} + .mx_AppearanceUserSettingsTab_fontSlider { display: flex; flex-direction: row; diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 771d2182ea..4d60550e02 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -60,6 +60,8 @@ interface IProps extends React.InputHTMLAttributes { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addlClassName = tooltipClassName ? tooltipClassName : ''; + const addClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = ; } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 6c70e89e28..3259c74ff4 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -354,6 +354,7 @@ export default class AppearanceUserSettingsTab extends React.Component From 045217ee8db1ddc615c6bc7d674f1c123366efc4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 17:46:16 +0100 Subject: [PATCH 005/106] fix style --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index ce99b85d35..8877535d6c 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AppearanceUserSettingsTab_fontSlider { - @mixin mx_Settings_fullWidthField; -} - .mx_AppearanceUserSettingsTab .mx_Field { width: 256px; } .mx_AppearanceUserSettingsTab_fontSlider { + @mixin mx_Settings_fullWidthField; display: flex; flex-direction: row; align-items: center; From dd23a50a3cccb50bfe4c90d0009f2a5951bbccdb Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:40:30 +0100 Subject: [PATCH 006/106] Upgrade matrix-js-sdk to 7.0.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5f9b7dde1f..98d7f7a6fa 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "7.0.0-rc.1", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index d2d53692b5..2bb99f4602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,9 +5820,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "6.2.2" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1c194e81637fb07fe6ad67cda33be0d5d4c10115" +matrix-js-sdk@7.0.0-rc.1: + version "7.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0-rc.1.tgz#95a258218f5c5ec73ec4be510b28768c35809a0b" + integrity sha512-1znl0d2UxU6Mmimy+pMSQP1lQfsmDb9jxiKV5sfMvTBsLtUE2cTqEBVDNVoOHL4UJ9U4oMLsrBgu3sELkgSJLQ== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 2533c33c54c870689960f4850e91e8b4be2c5e29 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:46:33 +0100 Subject: [PATCH 007/106] Prepare changelog for v2.8.0-rc.1 --- CHANGELOG.md | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089bfa73e0..9d7a73b264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,183 @@ +Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1) + + * Upgrade to JS SDK 7.0.0-rc.1 + * Fix Styled Checkbox and Radio Button disabled state + [\#4778](https://github.com/matrix-org/matrix-react-sdk/pull/4778) + * clean up and fix the isMasterRuleEnabled logic + [\#4782](https://github.com/matrix-org/matrix-react-sdk/pull/4782) + * Fix case-sensitivity of /me to match rest of slash commands + [\#4763](https://github.com/matrix-org/matrix-react-sdk/pull/4763) + * Add a 'show less' button to the new room list + [\#4765](https://github.com/matrix-org/matrix-react-sdk/pull/4765) + * Update from Weblate + [\#4781](https://github.com/matrix-org/matrix-react-sdk/pull/4781) + * Sticky and collapsing headers for new room list + [\#4758](https://github.com/matrix-org/matrix-react-sdk/pull/4758) + * Make the room list labs setting reload on change + [\#4780](https://github.com/matrix-org/matrix-react-sdk/pull/4780) + * Handle/hide old rooms in the room list + [\#4767](https://github.com/matrix-org/matrix-react-sdk/pull/4767) + * Add some media queries to improve UI on mobile (#3991) + [\#4656](https://github.com/matrix-org/matrix-react-sdk/pull/4656) + * Match fuzzy filtering a bit more reliably in the new room list + [\#4769](https://github.com/matrix-org/matrix-react-sdk/pull/4769) + * Improve Field ts definitions some more + [\#4777](https://github.com/matrix-org/matrix-react-sdk/pull/4777) + * Fix alignment of checkboxes in new room list's context menu + [\#4776](https://github.com/matrix-org/matrix-react-sdk/pull/4776) + * Fix Field ts def, fix LocalEchoWrapper and NotificationsEnabledController + [\#4775](https://github.com/matrix-org/matrix-react-sdk/pull/4775) + * Add presence indicators and globes to new room list + [\#4774](https://github.com/matrix-org/matrix-react-sdk/pull/4774) + * Include the sticky room when filtering in the new room list + [\#4772](https://github.com/matrix-org/matrix-react-sdk/pull/4772) + * Add a home button to the new room list menu when available + [\#4771](https://github.com/matrix-org/matrix-react-sdk/pull/4771) + * use group layout for search results + [\#4764](https://github.com/matrix-org/matrix-react-sdk/pull/4764) + * Fix m.id.phone spec compliance + [\#4757](https://github.com/matrix-org/matrix-react-sdk/pull/4757) + * User Info default power levels for ban/kick/redact to 50 as per spec + [\#4759](https://github.com/matrix-org/matrix-react-sdk/pull/4759) + * Match new room list's text search to old room list + [\#4768](https://github.com/matrix-org/matrix-react-sdk/pull/4768) + * Fix ordering of recent rooms in the new room list + [\#4766](https://github.com/matrix-org/matrix-react-sdk/pull/4766) + * Change theme selector to use new styled radio buttons + [\#4731](https://github.com/matrix-org/matrix-react-sdk/pull/4731) + * Use recovery keys over passphrases + [\#4686](https://github.com/matrix-org/matrix-react-sdk/pull/4686) + * Update from Weblate + [\#4760](https://github.com/matrix-org/matrix-react-sdk/pull/4760) + * Initial dark theme support for new room list + [\#4756](https://github.com/matrix-org/matrix-react-sdk/pull/4756) + * Support per-list options and algorithms on the new room list + [\#4754](https://github.com/matrix-org/matrix-react-sdk/pull/4754) + * Send read marker updates immediately after moving visually + [\#4755](https://github.com/matrix-org/matrix-react-sdk/pull/4755) + * Add a minimized view to the new room list + [\#4753](https://github.com/matrix-org/matrix-react-sdk/pull/4753) + * Fix e2e icon alignment in irc-layout + [\#4752](https://github.com/matrix-org/matrix-react-sdk/pull/4752) + * Add some resource leak protection to new room list badges + [\#4750](https://github.com/matrix-org/matrix-react-sdk/pull/4750) + * Fix read-receipt alignment + [\#4747](https://github.com/matrix-org/matrix-react-sdk/pull/4747) + * Show message previews on the new room list tiles + [\#4751](https://github.com/matrix-org/matrix-react-sdk/pull/4751) + * Fix various layout concerns with the new room list + [\#4749](https://github.com/matrix-org/matrix-react-sdk/pull/4749) + * Prioritize text on the clipboard over file + [\#4748](https://github.com/matrix-org/matrix-react-sdk/pull/4748) + * Move Settings flag to ts + [\#4729](https://github.com/matrix-org/matrix-react-sdk/pull/4729) + * Add a context menu to rooms in the new room list + [\#4743](https://github.com/matrix-org/matrix-react-sdk/pull/4743) + * Add hover states and basic context menu to new room list + [\#4742](https://github.com/matrix-org/matrix-react-sdk/pull/4742) + * Update resize handle for new designs in new room list + [\#4741](https://github.com/matrix-org/matrix-react-sdk/pull/4741) + * Improve general stability in the new room list + [\#4740](https://github.com/matrix-org/matrix-react-sdk/pull/4740) + * Reimplement breadcrumbs for new room list + [\#4735](https://github.com/matrix-org/matrix-react-sdk/pull/4735) + * Add styled radio buttons + [\#4744](https://github.com/matrix-org/matrix-react-sdk/pull/4744) + * Hide checkbox tick on dark backgrounds + [\#4730](https://github.com/matrix-org/matrix-react-sdk/pull/4730) + * Make checkboxes a11y friendly + [\#4746](https://github.com/matrix-org/matrix-react-sdk/pull/4746) + * EventIndex: Store and restore the encryption info for encrypted events. + [\#4738](https://github.com/matrix-org/matrix-react-sdk/pull/4738) + * Use IDestroyable instead of IDisposable + [\#4739](https://github.com/matrix-org/matrix-react-sdk/pull/4739) + * Add/improve badge counts in new room list + [\#4734](https://github.com/matrix-org/matrix-react-sdk/pull/4734) + * Convert FormattingUtils to TypeScript and add badge utility function + [\#4732](https://github.com/matrix-org/matrix-react-sdk/pull/4732) + * Add filtering and exploring to the new room list + [\#4736](https://github.com/matrix-org/matrix-react-sdk/pull/4736) + * Support prioritized room list filters + [\#4737](https://github.com/matrix-org/matrix-react-sdk/pull/4737) + * Clean up font scaling appearance + [\#4733](https://github.com/matrix-org/matrix-react-sdk/pull/4733) + * Add user menu to new room list + [\#4722](https://github.com/matrix-org/matrix-react-sdk/pull/4722) + * New room list basic styling and layout + [\#4711](https://github.com/matrix-org/matrix-react-sdk/pull/4711) + * Fix read receipt overlap + [\#4727](https://github.com/matrix-org/matrix-react-sdk/pull/4727) + * Load correct default font size + [\#4726](https://github.com/matrix-org/matrix-react-sdk/pull/4726) + * send state of lowBandwidth in rageshakes + [\#4724](https://github.com/matrix-org/matrix-react-sdk/pull/4724) + * Change internal font size from from 15 to 10 + [\#4725](https://github.com/matrix-org/matrix-react-sdk/pull/4725) + * Upgrade deps + [\#4723](https://github.com/matrix-org/matrix-react-sdk/pull/4723) + * Ensure active Jitsi conference is closed on widget pop-out + [\#4444](https://github.com/matrix-org/matrix-react-sdk/pull/4444) + * Introduce sticky rooms to the new room list + [\#4720](https://github.com/matrix-org/matrix-react-sdk/pull/4720) + * Handle remaining cases for room updates in new room list + [\#4721](https://github.com/matrix-org/matrix-react-sdk/pull/4721) + * Allow searching the emoji picker using other emoji + [\#4719](https://github.com/matrix-org/matrix-react-sdk/pull/4719) + * New room list scrolling and resizing + [\#4697](https://github.com/matrix-org/matrix-react-sdk/pull/4697) + * Don't show FormatBar if composer is empty + [\#4696](https://github.com/matrix-org/matrix-react-sdk/pull/4696) + * Split the left panel into new and old for new room list designs + [\#4687](https://github.com/matrix-org/matrix-react-sdk/pull/4687) + * Fix compact layout regression + [\#4712](https://github.com/matrix-org/matrix-react-sdk/pull/4712) + * fix emoji in safari + [\#4710](https://github.com/matrix-org/matrix-react-sdk/pull/4710) + * Fix not being able to dismiss new login toasts + [\#4709](https://github.com/matrix-org/matrix-react-sdk/pull/4709) + * Fix exceptions from Tooltip + [\#4708](https://github.com/matrix-org/matrix-react-sdk/pull/4708) + * Stop removing variation selector from quick reactions + [\#4707](https://github.com/matrix-org/matrix-react-sdk/pull/4707) + * Tidy up continuation algorithm and make it work for hidden profile changes + [\#4704](https://github.com/matrix-org/matrix-react-sdk/pull/4704) + * Profile settings should never show a disambiguated display name + [\#4699](https://github.com/matrix-org/matrix-react-sdk/pull/4699) + * Prevent (double) 4S bootstrap from RestoreKeyBackupDialog + [\#4701](https://github.com/matrix-org/matrix-react-sdk/pull/4701) + * Stop checkbox styling bleeding through room address selector + [\#4691](https://github.com/matrix-org/matrix-react-sdk/pull/4691) + * Center HeaderButtons + [\#4695](https://github.com/matrix-org/matrix-react-sdk/pull/4695) + * Add .well-known option to control default e2ee behaviour + [\#4605](https://github.com/matrix-org/matrix-react-sdk/pull/4605) + * Add max-width to right and left panels + [\#4692](https://github.com/matrix-org/matrix-react-sdk/pull/4692) + * Fix login loop where the sso flow returns to `#/login` + [\#4685](https://github.com/matrix-org/matrix-react-sdk/pull/4685) + * Don't clear MAU toasts when a successful sync comes in + [\#4690](https://github.com/matrix-org/matrix-react-sdk/pull/4690) + * Add initial filtering support to new room list + [\#4681](https://github.com/matrix-org/matrix-react-sdk/pull/4681) + * Bubble up a decline-to-render of verification events to outside wrapper + [\#4664](https://github.com/matrix-org/matrix-react-sdk/pull/4664) + * upgrade to twemoji 13.0.0 + [\#4672](https://github.com/matrix-org/matrix-react-sdk/pull/4672) + * Apply FocusLock to ImageView to capture Escape handling + [\#4666](https://github.com/matrix-org/matrix-react-sdk/pull/4666) + * Fix the 'complete security' screen + [\#4689](https://github.com/matrix-org/matrix-react-sdk/pull/4689) + * add null-guard for Autocomplete containerRef + [\#4688](https://github.com/matrix-org/matrix-react-sdk/pull/4688) + * Remove legacy codepaths for Unknown Device Error (UDE/UDD) handling + [\#4660](https://github.com/matrix-org/matrix-react-sdk/pull/4660) + * Remove feature_cross_signing + [\#4655](https://github.com/matrix-org/matrix-react-sdk/pull/4655) + * Autocomplete: use scrollIntoView for auto-scroll to fix it + [\#4670](https://github.com/matrix-org/matrix-react-sdk/pull/4670) + Changes in [2.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.2) (2020-06-16) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.1...v2.7.2) From b5aa66015c38748f1bc59a386cff8a7979903e36 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:46:34 +0100 Subject: [PATCH 008/106] v2.8.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98d7f7a6fa..06c4c43622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.7.2", + "version": "2.8.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From e4d824839ac782966fe61343707f45c1797f92b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 09:35:11 +0100 Subject: [PATCH 009/106] Revert "Use recovery keys over passphrases" --- .../structures/auth/_CompleteSecurity.scss | 4 - .../_CreateSecretStorageDialog.scss | 35 +- src/CrossSigningManager.js | 20 +- .../CreateSecretStorageDialog.js | 464 +++++++++++++----- .../structures/auth/CompleteSecurity.js | 4 - .../structures/auth/SetupEncryptionBody.js | 153 +----- .../keybackup/RestoreKeyBackupDialog.js | 2 +- .../AccessSecretStorageDialog.js | 9 +- .../views/settings/CrossSigningPanel.js | 2 +- src/i18n/strings/en_EN.json | 47 +- src/stores/SetupEncryptionStore.js | 64 +-- test/end-to-end-tests/src/usecases/signup.js | 23 +- 12 files changed, 400 insertions(+), 427 deletions(-) diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index b0462db477..f742be70e4 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -98,7 +98,3 @@ limitations under the License. } } } - -.mx_CompleteSecurity_resetText { - padding-top: 20px; -} diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 9f1d0f4998..63e5a3de09 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -73,42 +73,33 @@ limitations under the License. margin-left: 20px; } +.mx_CreateSecretStorageDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + .mx_CreateSecretStorageDialog_recoveryKeyContainer { - width: 380px; - margin-left: auto; - margin-right: auto; + display: flex; } .mx_CreateSecretStorageDialog_recoveryKey { - font-weight: bold; - text-align: center; + width: 262px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - border-radius: 6px; - word-spacing: 1em; - margin-bottom: 20px; + margin-right: 12px; } .mx_CreateSecretStorageDialog_recoveryKeyButtons { + flex: 1; display: flex; - justify-content: space-between; align-items: center; } .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - width: 160px; - padding-left: 0px; - padding-right: 0px; + margin-right: 10px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons button { + flex: 1; white-space: nowrap; } - -.mx_CreateSecretStorageDialog_continueSpinner { - margin-top: 33px; - text-align: right; -} - -.mx_CreateSecretStorageDialog_continueSpinner img { - width: 20px; - height: 20px; -} diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index d40f820ac0..c37d0f8bf5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,8 +30,6 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // operation ends. let secretStorageKeys = {}; let secretStorageBeingAccessed = false; -// Stores the 'passphraseOnly' option for the active storage access operation -let passphraseOnlyOption = null; function isCachingAllowed() { return ( @@ -101,7 +99,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); return await MatrixClientPeg.get().checkSecretStorageKey(key, info); }, - passphraseOnly: passphraseOnlyOption, }, /* className= */ null, /* isPriorityModal= */ false, @@ -216,27 +213,19 @@ export async function promptForBackupPassphrase() { * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. - * @param {object} [opts] Named options - * @param {bool} [opts.forceReset] Reset secret storage even if it's already set up - * @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client - * already has available. If a key is not supplied here, the user will be prompted. - * @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys + * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage( - func = async () => { }, opts = {}, -) { +export async function accessSecretStorage(func = async () => { }, forceReset = false) { const cli = MatrixClientPeg.get(); secretStorageBeingAccessed = true; - passphraseOnlyOption = opts.passphraseOnly; - secretStorageKeys = Object.assign({}, opts.withKeys || {}); try { - if (!await cli.hasSecretStorageKey() || opts.forceReset) { + if (!await cli.hasSecretStorageKey() || forceReset) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), { - force: opts.forceReset, + force: forceReset, }, null, /* priority = */ false, /* static = */ true, ); @@ -274,6 +263,5 @@ export async function accessSecretStorage( if (!isCachingAllowed()) { secretStorageKeys = {}; } - passphraseOnlyOption = null; } } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 192427d384..d7b79c2cfa 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -20,23 +20,25 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import FileSaver from 'file-saver'; -import {_t} from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; -import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; -import DialogButtons from "../../../../components/views/elements/DialogButtons"; -import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; - +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; const PHASE_MIGRATE = 2; -const PHASE_INTRO = 3; -const PHASE_SHOWKEY = 4; -const PHASE_STORING = 5; -const PHASE_CONFIRM_SKIP = 6; +const PHASE_PASSPHRASE = 3; +const PHASE_PASSPHRASE_CONFIRM = 4; +const PHASE_SHOWKEY = 5; +const PHASE_KEEPITSAFE = 6; +const PHASE_STORING = 7; +const PHASE_DONE = 8; +const PHASE_CONFIRM_SKIP = 9; + +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. /* * Walks the user through the process of creating a passphrase to guard Secure @@ -63,32 +65,34 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.state = { phase: PHASE_LOADING, - downloaded: false, + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', copied: false, + downloaded: false, backupInfo: null, - backupInfoFetched: false, - backupInfoFetchError: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password - // for /keys/device_signing/upload? (If we have an account password, we - // assume that it can) + // for /keys/device_signing/upload? canUploadKeysWithPasswordOnly: null, - canUploadKeyCheckInProgress: false, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - // No toggle for this: if we really don't want one, remove it & just hard code true + // status of the key backup toggle switch useKeyBackup: true, }; - if (props.accountPassword) { - // If we have an account password, we assume we can upload keys with - // just a password (otherwise leave it as null so we poll to check) - this.state.canUploadKeysWithPasswordOnly = true; - } - this._passphraseField = createRef(); - this.loadData(); + this._fetchBackupInfo(); + if (this.state.accountPassword) { + // If we have an account password in memory, let's simplify and + // assume it means password auth is also supported for device + // signing key upload as well. This avoids hitting the server to + // test auth flows, which may be slow under high load. + this.state.canUploadKeysWithPasswordOnly = true; + } else { + this._queryKeyUploadAuth(); + } MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } @@ -105,11 +109,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); + const { force } = this.props; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; + this.setState({ - backupInfoFetched: true, + phase, backupInfo, backupSigStatus, - backupInfoFetchError: null, }); return { @@ -117,25 +123,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { backupSigStatus, }; } catch (e) { - this.setState({backupInfoFetchError: e}); + this.setState({phase: PHASE_LOADERROR}); } } async _queryKeyUploadAuth() { try { - this.setState({canUploadKeyCheckInProgress: true}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require // UI auth to upload device signing keys. If we do, we upload // no keys which would be a no-op. console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - this.setState({canUploadKeyCheckInProgress: false}); } catch (error) { if (!error.data || !error.data.flows) { console.log("uploadDeviceSigningKeys advertised no flows!"); - this.setState({ - canUploadKeyCheckInProgress: false, - }); return; } const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { @@ -143,18 +144,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); this.setState({ canUploadKeysWithPasswordOnly, - canUploadKeyCheckInProgress: false, }); } } - async _createRecoveryKey() { - this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this.setState({ - phase: PHASE_SHOWKEY, - }); - } - _onKeyBackupStatusChange = () => { if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } @@ -163,6 +156,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } + _onUseKeyBackupChange = (enabled) => { + this.setState({ + useKeyBackup: enabled, + }); + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -172,15 +171,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onIntroContinueClick = () => { - this._createRecoveryKey(); - } - _onCopyClick = () => { const successful = copyNode(this._recoveryKeyNode); if (successful) { this.setState({ copied: true, + phase: PHASE_KEEPITSAFE, }); } } @@ -190,8 +186,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); + this.setState({ downloaded: true, + phase: PHASE_KEEPITSAFE, }); } @@ -247,9 +245,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _bootstrapSecretStorage = async () => { this.setState({ - // we use LOADING here rather than STORING as STORING still shows the 'show key' - // screen which is not relevant: LOADING is just a generic spinner. - phase: PHASE_LOADING, + phase: PHASE_STORING, error: null, }); @@ -290,7 +286,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - this.props.onFinished(true); + this.setState({ + phase: PHASE_DONE, + }); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ @@ -309,6 +307,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(false); } + _onDone = () => { + this.props.onFinished(true); + } + _restoreBackup = async () => { // It's possible we'll need the backup key later on for bootstrapping, // so let's stash it here, rather than prompting for it twice. @@ -335,41 +337,88 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onShowKeyContinueClick = () => { - this._bootstrapSecretStorage(); - } - _onLoadRetryClick = () => { - this.loadData(); - } - - async loadData() { this.setState({phase: PHASE_LOADING}); - const proms = []; - - if (!this.state.backupInfoFetched) proms.push(this._fetchBackupInfo()); - if (this.state.canUploadKeysWithPasswordOnly === null) proms.push(this._queryKeyUploadAuth()); - - await Promise.all(proms); - if (this.state.canUploadKeysWithPasswordOnly === null || this.state.backupInfoFetchError) { - this.setState({phase: PHASE_LOADERROR}); - } else if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); - } + this._fetchBackupInfo(); } _onSkipSetupClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } - _onGoBackClick = () => { - if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); + _onSetUpClick = () => { + this.setState({phase: PHASE_PASSPHRASE}); + } + + _onSkipPassPhraseClick = async () => { + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseNextClick = async (e) => { + e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting + + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + }; + + _onPassPhraseConfirmNextClick = async (e) => { + e.preventDefault(); + + if (this.state.passPhrase !== this.state.passPhraseConfirm) return; + + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onSetAgainClick = () => { + this.setState({ + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', + phase: PHASE_PASSPHRASE, + }); + } + + _onKeepItSafeBackClick = () => { + this.setState({ + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + + _onPassPhraseChange = (e) => { + this.setState({ + passPhrase: e.target.value, + }); + } + + _onPassPhraseConfirmChange = (e) => { + this.setState({ + passPhraseConfirm: e.target.value, + }); } _onAccountPasswordChange = (e) => { @@ -384,14 +433,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // Once we're confident enough in this (and it's supported enough) we can do // it automatically. // https://github.com/vector-im/riot-web/issues/11696 + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); let authPrompt; let nextCaption = _t("Next"); - if (!this.state.backupSigStatus.usable) { - authPrompt = null; - nextCaption = _t("Upload"); - } else if (this.state.canUploadKeysWithPasswordOnly && !this.props.accountPassword) { + if (this.state.canUploadKeysWithPasswordOnly) { authPrompt =
{_t("Enter your account password to confirm the upgrade:")}
; + } else if (!this.state.backupSigStatus.usable) { + authPrompt =
+
{_t("Restore your key backup to upgrade your encryption")}
+
; + nextCaption = _t("Restore"); } else { authPrompt =

{_t("You'll need to authenticate with the server to confirm the upgrade.")} @@ -411,9 +463,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return

{_t( - "Upgrade your Recovery Key to store encryption keys & secrets " + - "with your account data. If you lose access to this login you'll " + - "need it to unlock your data.", + "Upgrade this session to allow it to verify other sessions, " + + "granting them access to encrypted messages and marking them " + + "as trusted for other users.", )}

{authPrompt}
; } - _renderPhaseShowKey() { - let continueButton; - if (this.state.phase === PHASE_SHOWKEY) { - continueButton = +

{_t( + "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + + "This should be different to your account password:", + )}

+ +
+ +
+ + + + ; - } else { - continueButton =
- -
; + disabled={!this.state.passPhraseValid} + > + +
+ +
+ {_t("Advanced")} + + {_t("Set up with a recovery key")} + +
+ ; + } + + _renderPhasePassPhraseConfirm() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('views.elements.Field'); + + let matchText; + let changeText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); + } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { + // only tell them they're wrong if they've actually gone wrong. + // Security concious readers will note that if you left riot-web unattended + // on this screen, this would make it easy for a malicious person to guess + // your passphrase one letter at a time, but they could get this faster by + // just opening the browser's developer tools and reading it. + // Note that not having typed anything at all will not hit this clause and + // fall through so empty box === no hint. + matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } + let passPhraseMatch = null; + if (matchText) { + passPhraseMatch =
+
{matchText}
+
+ + {changeText} + +
+
; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Enter your recovery passphrase a second time to confirm it.", + )}

+
+ +
+ {passPhraseMatch} +
+
+ + + +
; + } + + _renderPhaseShowKey() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return

{_t( - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", + "Your recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your recovery passphrase.", + )}

+

{_t( + "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

+
+ {_t("Your recovery key")} +
{this._recoveryKey.encodedPrivateKey}
- - {_t("Download")} - - {_t("or")} - {this.state.copied ? _t("Copied!") : _t("Copy")} + {_t("Copy")} + + + {_t("Download")}
- {continueButton} +
; + } + + _renderPhaseKeepItSafe() { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {introText} +
    +
  • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
  • +
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • +
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • +
+ + +
; } @@ -483,6 +671,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseLoadError() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t("Unable to query secret storage status")}

@@ -495,44 +684,29 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - _renderPhaseIntro() { - let cancelButton; - if (this.props.force) { - // if this is a forced key reset then aborting will just leave the old keys - // in place, and is thereforece just 'cancel' - cancelButton = ; - } else { - // if it's setting up from scratch then aborting leaves the user without - // crypto set up, so they skipping the setup. - cancelButton = ; - } - + _renderPhaseDone() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( - "Create a Recovery Key to store encryption keys & secrets with your account data. " + - "If you lose access to this login you’ll need it to unlock your data.", + "You can now verify your other devices, " + + "and other users to keep your chats safe.", )}

-
- - {cancelButton} - -
+
; } _renderPhaseSkipConfirm() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
{_t( "Without completing security on this session, it won’t have " + "access to encrypted messages.", )} @@ -542,15 +716,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { - case PHASE_INTRO: - return _t('Create a Recovery Key'); case PHASE_MIGRATE: - return _t('Upgrade your Recovery Key'); + return _t('Upgrade your encryption'); + case PHASE_PASSPHRASE: + return _t('Set up encryption'); + case PHASE_PASSPHRASE_CONFIRM: + return _t('Confirm recovery passphrase'); case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: + case PHASE_KEEPITSAFE: + return _t('Make a copy of your recovery key'); case PHASE_STORING: - return _t('Store your Recovery Key'); + return _t('Setting up keys'); + case PHASE_DONE: + return _t("You're done!"); default: return ''; } @@ -561,6 +741,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

{_t("Unable to set up secret storage")}

@@ -579,16 +760,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADERROR: content = this._renderPhaseLoadError(); break; - case PHASE_INTRO: - content = this._renderPhaseIntro(); - break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; + case PHASE_PASSPHRASE: + content = this._renderPhasePassPhrase(); + break; + case PHASE_PASSPHRASE_CONFIRM: + content = this._renderPhasePassPhraseConfirm(); + break; case PHASE_SHOWKEY: - case PHASE_STORING: content = this._renderPhaseShowKey(); break; + case PHASE_KEEPITSAFE: + content = this._renderPhaseKeepItSafe(); + break; + case PHASE_STORING: + content = this._renderBusyPhase(); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; case PHASE_CONFIRM_SKIP: content = this._renderPhaseSkipConfirm(); break; @@ -605,7 +797,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { onFinished={this.props.onFinished} title={this._titleForPhase(this.state.phase)} headerImage={headerImage} - hasCancel={this.props.hasCancel} + hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} >
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index e38ecd3eac..c73691611d 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -21,7 +21,6 @@ import * as sdk from '../../../index'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, @@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component { if (phase === PHASE_INTRO) { icon = ; title = _t("Verify this login"); - } else if (phase === PHASE_RECOVERY_KEY) { - icon = ; - title = _t("Recovery Key"); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 7886ed26dd..26534c6e02 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -19,26 +19,15 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -import withValidation from '../../views/elements/Validation'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; -function keyHasPassphrase(keyInfo) { - return ( - keyInfo.passphrase && - keyInfo.passphrase.salt && - keyInfo.passphrase.iterations - ); -} - export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, @@ -56,11 +45,6 @@ export default class SetupEncryptionBody extends React.Component { // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, - recoveryKey: '', - // whether the recovery key is a valid recovery key - recoveryKeyValid: null, - // whether the recovery key is the correct key or not - recoveryKeyCorrect: null, }; } @@ -83,19 +67,9 @@ export default class SetupEncryptionBody extends React.Component { store.stop(); } - _onResetClick = () => { + _onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); - store.startKeyReset(); - } - - _onUseRecoveryKeyClick = async () => { - const store = SetupEncryptionStore.sharedInstance(); - store.useRecoveryKey(); - } - - _onRecoveryKeyCancelClick() { - const store = SetupEncryptionStore.sharedInstance(); - store.cancelUseRecoveryKey(); + store.usePassPhrase(); } onSkipClick = () => { @@ -118,66 +92,6 @@ export default class SetupEncryptionBody extends React.Component { store.done(); } - _onUsePassphraseClick = () => { - const store = SetupEncryptionStore.sharedInstance(); - store.usePassPhrase(); - } - - _onRecoveryKeyChange = (e) => { - this.setState({recoveryKey: e.target.value}); - } - - _onRecoveryKeyValidate = async (fieldState) => { - const result = await this._validateRecoveryKey(fieldState); - this.setState({recoveryKeyValid: result.valid}); - return result; - } - - _validateRecoveryKey = withValidation({ - rules: [ - { - key: "required", - test: async (state) => { - try { - const decodedKey = decodeRecoveryKey(state.value); - const correct = await MatrixClientPeg.get().checkSecretStorageKey( - decodedKey, SetupEncryptionStore.sharedInstance().keyInfo, - ); - this.setState({ - recoveryKeyValid: true, - recoveryKeyCorrect: correct, - }); - return correct; - } catch (e) { - this.setState({ - recoveryKeyValid: false, - recoveryKeyCorrect: false, - }); - return false; - } - }, - invalid: function() { - if (this.state.recoveryKeyValid) { - return _t("This isn't the recovery key for your account"); - } else { - return _t("This isn't a valid recovery key"); - } - }, - valid: function() { - return _t("Looks good!"); - }, - }, - ], - }) - - _onRecoveryKeyFormSubmit = (e) => { - e.preventDefault(); - if (!this.state.recoveryKeyCorrect) return; - - const store = SetupEncryptionStore.sharedInstance(); - store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey)); - } - render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component { member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; } else if (phase === PHASE_INTRO) { - const store = SetupEncryptionStore.sharedInstance(); - let recoveryKeyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); - } else { - recoveryKeyPrompt = _t("Use Recovery Key"); - } return (

{_t( @@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {

- - {recoveryKeyPrompt} + + {_t("Use Recovery Passphrase or Key")} {_t("Skip")}
-
{_t( - "If you've forgotten your recovery key you can " + - "", {}, { - button: sub => - {sub} - , - }, - )}
); - } else if (phase === PHASE_RECOVERY_KEY) { - const store = SetupEncryptionStore.sharedInstance(); - let keyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - keyPrompt = _t( - "Enter your Recovery Key or enter a Recovery Passphrase to continue.", {}, - { - a: sub => {sub}, - }, - ); - } else { - keyPrompt = _t("Enter your Recovery Key to continue."); - } - - const Field = sdk.getComponent('elements.Field'); - return
-

{keyPrompt}

-
- -
-
- - {_t("Cancel")} - - - {_t("Continue")} - -
-
; } else if (phase === PHASE_DONE) { let message; if (this.state.backupInfo) { diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 87ba6f7396..dd34dfbbf0 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { _onResetRecoveryClick = () => { this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 43697f8ee7..e2ceadfbb9 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent { keyInfo: PropTypes.object.isRequired, // Function from one of { passphrase, recoveryKey } -> boolean checkPrivateKey: PropTypes.func.isRequired, - // If true, only prompt for a passphrase and do not offer to restore with - // a recovery key or reset keys. - passphraseOnly: PropTypes.bool, } constructor(props) { @@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { _onResetRecoveryClick = () => { // Re-enter the access flow, but resetting storage this time around. this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { @@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={this.state.passPhrase.length === 0} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery passphrase you can "+ "use your recovery key or " + "set up new recovery options." @@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={!this.state.recoveryKeyValid} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery key you can "+ "." , {}, { diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index f48ee3cd0d..7eb239cbca 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent { _bootstrapSecureSecretStorage = async (forceReset=false) => { this.setState({ error: null }); try { - await accessSecretStorage(() => undefined, {forceReset}); + await accessSecretStorage(() => undefined, forceReset); } catch (e) { this.setState({ error: e }); console.error("Error bootstrapping secret storage", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5de33ada55..9a41517664 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2068,7 +2068,6 @@ "Account settings": "Account settings", "Could not load user profile": "Could not load user profile", "Verify this login": "Verify this login", - "Recovery Key": "Recovery Key", "Session verified": "Session verified", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", @@ -2122,16 +2121,10 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", - "This isn't the recovery key for your account": "This isn't the recovery key for your account", - "This isn't a valid recovery key": "This isn't a valid recovery key", - "Looks good!": "Looks good!", - "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", - "Use Recovery Key": "Use Recovery Key", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", - "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Enter your Recovery Key or enter a Recovery Passphrase to continue.", - "Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.", + "Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", @@ -2175,43 +2168,47 @@ "Confirm encryption setup": "Confirm encryption setup", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", + "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", + "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", - "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.", - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", - "Download": "Download", - "Copy": "Copy", - "Unable to query secret storage status": "Unable to query secret storage status", - "Retry": "Retry", - "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.", - "Create a Recovery Key": "Create a Recovery Key", - "Upgrade your Recovery Key": "Upgrade your Recovery Key", - "Store your Recovery Key": "Store your Recovery Key", - "Unable to set up secret storage": "Unable to set up secret storage", - "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", - "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", + "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:", "Enter a recovery passphrase": "Enter a recovery passphrase", "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", + "Back up encrypted message keys": "Back up encrypted message keys", "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", - "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", - "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", + "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", + "Confirm your recovery passphrase": "Confirm your recovery passphrase", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Your recovery key": "Your recovery key", + "Copy": "Copy", + "Download": "Download", "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Unable to query secret storage status": "Unable to query secret storage status", + "Retry": "Retry", + "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", + "Upgrade your encryption": "Upgrade your encryption", + "Confirm recovery passphrase": "Confirm recovery passphrase", + "Make a copy of your recovery key": "Make a copy of your recovery key", + "You're done!": "You're done!", + "Unable to set up secret storage": "Unable to set up secret storage", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", + "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", + "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", - "Confirm your recovery passphrase": "Confirm your recovery passphrase", - "Make a copy of your recovery key": "Make a copy of your recovery key", "Starting backup...": "Starting backup...", "Success!": "Success!", "Create key backup": "Create key backup", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index cc64e24a03..ae1f998b02 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -20,11 +20,10 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; export const PHASE_INTRO = 0; -export const PHASE_RECOVERY_KEY = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export const PHASE_BUSY = 1; +export const PHASE_DONE = 2; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 3; +export const PHASE_FINISHED = 4; //UX can be closed export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { @@ -37,19 +36,11 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_BUSY; + this.phase = PHASE_INTRO; this.verificationRequest = null; this.backupInfo = null; - - // ID of the key that the secrets we want are encrypted with - this.keyId = null; - // Descriptor of the key that the secrets we want are encrypted with - this.keyInfo = null; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); - - this.fetchKeyInfo(); } stop() { @@ -66,49 +57,7 @@ export class SetupEncryptionStore extends EventEmitter { } } - async fetchKeyInfo() { - const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); - if (Object.keys(keys).length === 0) { - this.keyId = null; - this.keyInfo = null; - } else { - // If the secret is stored under more than one key, we just pick an arbitrary one - this.keyId = Object.keys(keys)[0]; - this.keyInfo = keys[this.keyId]; - } - - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async startKeyReset() { - try { - await accessSecretStorage(() => {}, {forceReset: true}); - // If the keys are reset, the trust status event will fire and we'll change state - } catch (e) { - // dialog was cancelled - stay on the current screen - } - } - - async useRecoveryKey() { - this.phase = PHASE_RECOVERY_KEY; - this.emit("update"); - } - - cancelUseRecoveryKey() { - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async setupWithRecoveryKey(recoveryKey) { - this.startTrustCheck({[this.keyId]: recoveryKey}); - } - async usePassPhrase() { - this.startTrustCheck(); - } - - async startTrustCheck(withKeys) { this.phase = PHASE_BUSY; this.emit("update"); const cli = MatrixClientPeg.get(); @@ -135,9 +84,6 @@ export class SetupEncryptionStore extends EventEmitter { // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } - }, { - withKeys, - passphraseOnly: true, }).catch(reject); } catch (e) { console.error(e); diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 2859aadbda..aa9f6b7efa 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -79,7 +79,20 @@ module.exports = async function signup(session, username, password, homeserver) const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); - const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + //plow through cross-signing setup by entering arbitrary details + //TODO: It's probably important for the tests to know the passphrase + const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/ + let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + await xsignContButton.click(); + + //repeat passphrase entry + passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await xsignContButton.click(); //ignore the recovery key @@ -88,11 +101,13 @@ module.exports = async function signup(session, username, password, homeserver) await copyButton.click(); //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query( - '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', - ); + const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); await copyContinueButton.click(); + //acknowledge that we're done cross-signing setup and our keys are safe + const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); + await doneOkButton.click(); + //wait for registration to finish so the hash gets set //onhashchange better? From c690cfc6c59dab6c366c17a284261a68035fb10f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 18 Jun 2020 21:57:33 -0400 Subject: [PATCH 010/106] mark messages with a black shield if the megolm session isn't trusted --- res/css/views/rooms/_EventTile.scss | 5 +++ src/components/views/rooms/E2EIcon.js | 1 + src/components/views/rooms/EventTile.js | 51 ++++++++++++++++++------- src/i18n/strings/en_EN.json | 1 + 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 40a80f17bb..f6cfd9e1d1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -354,6 +354,11 @@ limitations under the License. opacity: 1; } +.mx_EventTile_e2eIcon_unauthenticated { + background-image: url('$(res)/img/e2e/normal.svg'); + opacity: 1; +} + .mx_EventTile_e2eIcon_hidden { display: none; } diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index bf65c7fb7c..254e28dffa 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -28,6 +28,7 @@ export const E2E_STATE = { WARNING: "warning", UNKNOWN: "unknown", NORMAL: "normal", + UNAUTHENTICATED: "unauthenticated", }; const crossSigningUserTitles = { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7508cf3372..88c4ed2e7d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -313,35 +313,52 @@ export default createReactClass({ return; } - // If we directly trust the device, short-circuit here - const verified = await this.context.isEventSenderVerified(mxEvent); - if (verified) { + const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent); + const senderId = mxEvent.getSender(); + const userTrust = this.context.checkUserTrust(senderId); + + if (encryptionInfo.mismatchedSender) { + // something definitely wrong is going on here this.setState({ - verified: E2E_STATE.VERIFIED, - }, () => { - // Decryption may have caused a change in size - this.props.onHeightChanged(); - }); + verified: E2E_STATE.WARNING, + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } - if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) { + if (!userTrust.isCrossSigningVerified()) { + // user is not verified, so default to everything is normal this.setState({ verified: E2E_STATE.NORMAL, - }, this.props.onHeightChanged); + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } - const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); + const eventSenderTrust = this.context.checkDeviceTrust( + senderId, encryptionInfo.sender.deviceId, + ); if (!eventSenderTrust) { this.setState({ verified: E2E_STATE.UNKNOWN, - }, this.props.onHeightChanged); // Decryption may have cause a change in size + }, this.props.onHeightChanged); // Decryption may have caused a change in size + return; + } + + if (!eventSenderTrust.isVerified()) { + this.setState({ + verified: E2E_STATE.WARNING, + }, this.props.onHeightChanged); // Decryption may have caused a change in size + return; + } + + if (!encryptionInfo.authenticated) { + this.setState({ + verified: E2E_STATE.UNAUTHENTICATED, + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } this.setState({ - verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING, + verified: E2E_STATE.VERIFIED, }, this.props.onHeightChanged); // Decryption may have caused a change in size }, @@ -526,6 +543,8 @@ export default createReactClass({ return; // no icon if we've not even cross-signed the user } else if (this.state.verified === E2E_STATE.VERIFIED) { return; // no icon for verified + } else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) { + return (); } else if (this.state.verified === E2E_STATE.UNKNOWN) { return (); } else { @@ -976,6 +995,12 @@ function E2ePadlockUnknown(props) { ); } +function E2ePadlockUnauthenticated(props) { + return ( + + ); +} + class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 396c3f9111..2dcca91e82 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1023,6 +1023,7 @@ "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", "Encrypted by a deleted session": "Encrypted by a deleted session", + "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", "Please select the destination room for this message": "Please select the destination room for this message", "Invite only": "Invite only", "Scroll to most recent messages": "Scroll to most recent messages", From 3d7427cccafae58ebcae46c7f71c338b8db272aa Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 11:39:11 +0100 Subject: [PATCH 011/106] Fix semicolons --- src/components/views/elements/Field.tsx | 4 ++-- .../tabs/user/AppearanceUserSettingsTab.tsx | 18 +++++++++--------- .../payloads/UpdateFontSizePayload.ts | 2 +- .../payloads/UpdateSystemFontPayload.ts | 2 +- src/settings/watchers/FontWatcher.ts | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 9014e2c3c9..9d53576259 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -53,9 +53,9 @@ interface IProps { flagInvalid?: boolean; // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltipContent?: React.ReactNode, + tooltipContent?: React.ReactNode; // If specified the tooltip will be shown regardless of feedback - forceTooltipVisible?: boolean, + forceTooltipVisible?: boolean; // If specified alongside tooltipContent, the class name to apply to the // tooltip itself. tooltipClassName?: string; diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 10f5b4a5a4..5a08d99c19 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -47,13 +47,13 @@ interface IState extends IThemeState { // String displaying the current selected fontSize. // Needs to be string for things like '17.' without // trailing 0s. - fontSize: string, - customThemeUrl: string, - customThemeMessage: CustomThemeMessage, - useCustomFontSize: boolean, - useSystemFont: boolean, - systemFont: string, - showAdvanced: boolean, + fontSize: string; + customThemeUrl: string; + customThemeMessage: CustomThemeMessage; + useCustomFontSize: boolean; + useSystemFont: boolean; + systemFont: string; + showAdvanced: boolean; } export default class AppearanceUserSettingsTab extends React.Component { @@ -349,7 +349,7 @@ export default class AppearanceUserSettingsTab extends React.Component { this.setState({ systemFont: value.target.value, - }) + }); SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value); }} @@ -363,7 +363,7 @@ export default class AppearanceUserSettingsTab extends React.Component {toggle} {advanced} -
+
; } render() { diff --git a/src/dispatcher/payloads/UpdateFontSizePayload.ts b/src/dispatcher/payloads/UpdateFontSizePayload.ts index 3eac3e4607..6577acd594 100644 --- a/src/dispatcher/payloads/UpdateFontSizePayload.ts +++ b/src/dispatcher/payloads/UpdateFontSizePayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface UpdateFontSizePayload extends ActionPayload { - action: Action.UpdateFontSize, + action: Action.UpdateFontSize; /** * The font size to set the root to diff --git a/src/dispatcher/payloads/UpdateSystemFontPayload.ts b/src/dispatcher/payloads/UpdateSystemFontPayload.ts index 73475e10d5..aa59db5aa9 100644 --- a/src/dispatcher/payloads/UpdateSystemFontPayload.ts +++ b/src/dispatcher/payloads/UpdateSystemFontPayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface UpdateSystemFontPayload extends ActionPayload { - action: Action.UpdateSystemFont, + action: Action.UpdateSystemFont; /** * Specify whether to use a system font or the stylesheet font diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index cc843edb4d..9af5156704 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -38,7 +38,7 @@ export class FontWatcher implements IWatcher { this.setSystemFont({ useSystemFont: SettingsStore.getValue("useSystemFont"), font: SettingsStore.getValue("systemFont"), - }) + }); this.dispatcherRef = dis.register(this.onAction); } @@ -50,7 +50,7 @@ export class FontWatcher implements IWatcher { if (payload.action === Action.UpdateFontSize) { this.setRootFontSize(payload.size); } else if (payload.action === Action.UpdateSystemFont) { - this.setSystemFont(payload) + this.setSystemFont(payload); } }; @@ -65,5 +65,5 @@ export class FontWatcher implements IWatcher { private setSystemFont = ({useSystemFont, font}) => { document.body.style.fontFamily = useSystemFont ? font : ""; - } + }; } From ea929e575af9046d71a6089fba6e7646067607d0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 16:02:23 +0100 Subject: [PATCH 012/106] Fix MessageActionBar --- res/css/views/rooms/_IRCLayout.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 814a614007..94753f9473 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -121,11 +121,6 @@ $irc-line-height: $font-18px; } } - .mx_EventTile_line .mx_MessageActionBar, - .mx_EventTile_line .mx_ReplyThread_wrapper { - display: block; - } - .mx_EventTile_reply { order: 4; } From 7bc5ce7271b337d904ae880224973f08d4c42adf Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 16:05:32 +0100 Subject: [PATCH 013/106] Underscore actions --- src/dispatcher/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index a03c731818..379a0a4451 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -73,10 +73,10 @@ export enum Action { /** * Sets the apps root font size. Should be used with UpdateFontSizePayload */ - UpdateFontSize = "update-font-size", + UpdateFontSize = "update_font_size", /** * Sets a system font. Should be used with UpdateSystemFontPayload */ - UpdateSystemFont = "update-system-font", + UpdateSystemFont = "update_system_font", } From 099661c2aacd2eb360d8dcb6ac9e24972db16bc6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:18:38 -0600 Subject: [PATCH 014/106] Only fire setting changes for changed settings Previously we were firing updates for everything, which is bad. This has an effect of causing the room list to update itself every time the user goes to toggle some account settings. --- .../handlers/AccountSettingsHandler.js | 9 ++-- .../MatrixClientBackedSettingsHandler.js | 4 ++ .../handlers/RoomAccountSettingsHandler.js | 9 ++-- src/settings/handlers/RoomSettingsHandler.js | 13 +++-- src/utils/arrays.ts | 22 +++++++++ src/utils/objects.ts | 49 +++++++++++++++++++ 6 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 src/utils/objects.ts diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index fea2e92c62..7b78c39c3a 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; @@ -45,7 +46,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa newClient.on("accountData", this._onAccountData); } - _onAccountData(event) { + _onAccountData(event, prevEvent) { if (event.getType() === "org.matrix.preview_urls") { let val = event.getContent()['disable']; if (typeof(val) !== "boolean") { @@ -56,8 +57,10 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa this._watchers.notifyUpdate("urlPreviewsEnabled", null, SettingLevel.ACCOUNT, val); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this._watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val); } diff --git a/src/settings/handlers/MatrixClientBackedSettingsHandler.js b/src/settings/handlers/MatrixClientBackedSettingsHandler.js index effe7ae9a7..63725b4dff 100644 --- a/src/settings/handlers/MatrixClientBackedSettingsHandler.js +++ b/src/settings/handlers/MatrixClientBackedSettingsHandler.js @@ -42,6 +42,10 @@ export default class MatrixClientBackedSettingsHandler extends SettingsHandler { MatrixClientBackedSettingsHandler._instances.push(this); } + get client() { + return MatrixClientBackedSettingsHandler._matrixClient; + } + initMatrixClient() { console.warn("initMatrixClient not overridden"); } diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 1e9d3f7bed..1c818cef71 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; @@ -40,7 +41,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin newClient.on("Room.accountData", this._onAccountData); } - _onAccountData(event, room) { + _onAccountData(event, room, prevEvent) { const roomId = room.roomId; if (event.getType() === "org.matrix.room.preview_urls") { @@ -55,8 +56,10 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin } else if (event.getType() === "org.matrix.room.color_scheme") { this._watchers.notifyUpdate("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent()); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_ACCOUNT, val); } diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 6407818450..4116f26220 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; /** * Gets and sets settings at the "room" level. @@ -38,8 +39,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl newClient.on("RoomState.events", this._onEvent); } - _onEvent(event) { + _onEvent(event, state, prevEvent) { const roomId = event.getRoomId(); + const room = this.client.getRoom(roomId); + if (!room) throw new Error(`Unknown room caused state update: ${roomId}`); + + if (state !== room.currentState) return; // ignore state updates which are not current if (event.getType() === "org.matrix.room.preview_urls") { let val = event.getContent()['disable']; @@ -51,8 +56,10 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, SettingLevel.ROOM, val); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM, event.getContent()[settingName]); } } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index fea376afcd..8175d89464 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -46,6 +46,28 @@ export function arrayDiff(a: T[], b: T[]): { added: T[], removed: T[] } { }; } +/** + * Returns the union of two arrays. + * @param a The first array. Must be defined. + * @param b The second array. Must be defined. + * @returns The union of the arrays. + */ +export function arrayUnion(a: T[], b: T[]): T[] { + return a.filter(i => b.includes(i)); +} + +/** + * Merges arrays, deduping contents using a Set. + * @param a The arrays to merge. + * @returns The merged array. + */ +export function arrayMerge(...a: T[][]): T[] { + return Array.from(a.reduce((c, v) => { + v.forEach(i => c.add(i)); + return c; + }, new Set())); +} + /** * Helper functions to perform LINQ-like queries on arrays. */ diff --git a/src/utils/objects.ts b/src/utils/objects.ts new file mode 100644 index 0000000000..cbb311cc48 --- /dev/null +++ b/src/utils/objects.ts @@ -0,0 +1,49 @@ +/* +Copyright 2020 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. +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 { arrayDiff, arrayMerge, arrayUnion } from "./arrays"; + +/** + * Determines the keys added, changed, and removed between two objects. + * For changes, simple triple equal comparisons are done, not in-depth + * tree checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The difference between the keys of each object. + */ +export function objectDiff(a: any, b: any): { changed: string[], added: string[], removed: string[] } { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + const keyDiff = arrayDiff(aKeys, bKeys); + const possibleChanges = arrayUnion(aKeys, bKeys); + const changes = possibleChanges.filter(k => a[k] !== b[k]); + + return {changed: changes, added: keyDiff.added, removed: keyDiff.removed}; +} + +/** + * Gets all the key changes (added, removed, or value difference) between + * two objects. Triple equals is used to compare values, not in-depth tree + * checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The keys which have been added, removed, or changed between the + * two objects. + */ +export function objectKeyChanges(a: any, b: any): string[] { + const diff = objectDiff(a, b); + return arrayMerge(diff.removed, diff.added, diff.changed); +} From 30d8dc06fcad02b0a2012d114b5a6fb5ecbf498c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:43:05 -0600 Subject: [PATCH 015/106] Increase bold weight for unread rooms For https://github.com/vector-im/riot-web/issues/14084 --- res/css/views/rooms/_RoomTile2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 001499fea5..a97d1fd5b9 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -67,7 +67,7 @@ limitations under the License. } .mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents { - font-weight: 600; + font-weight: 700; } .mx_RoomTile2_messagePreview { From eeb408a0810e96afbb178652acbace7b0b6b0841 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:44:37 -0600 Subject: [PATCH 016/106] Update badge logic for new setting and behaviour For https://github.com/vector-im/riot-web/issues/14084 --- .../views/rooms/NotificationBadge.tsx | 50 ++++++++++++++++--- src/components/views/rooms/RoomSublist2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 8 ++- src/settings/Settings.js | 5 ++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index b742f8e8e7..65ccc88c5f 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -32,13 +32,14 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; +import SettingsStore from "../../../settings/SettingsStore"; export const NOTIFICATION_STATE_UPDATE = "update"; export enum NotificationColor { // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special - Bold, // no badge, show as unread + Bold, // no badge, show as unread // TODO: This goes away with new notification structures Grey, // unread notified messages Red, // unread pings } @@ -53,18 +54,45 @@ interface IProps { notification: INotificationState; /** - * If true, the badge will conditionally display a badge without count for the user. + * If true, the badge will show a count if at all possible. This is typically + * used to override the user's preference for things like room sublists. */ - allowNoCount: boolean; + forceCount: boolean; + + /** + * The room ID, if any, the badge represents. + */ + roomId?: string; } interface IState { + showCounts: boolean; // whether or not to show counts. Independent of props.forceCount } export default class NotificationBadge extends React.PureComponent { + private countWatcherRef: string; + constructor(props: IProps) { super(props); this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + + this.state = { + showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId), + }; + + this.countWatcherRef = SettingsStore.watchSetting( + "Notifications.alwaysShowBadgeCounts", this.roomId, + this.countPreferenceChanged, + ); + } + + private get roomId(): string { + // We should convert this to null for safety with the SettingsStore + return this.props.roomId || null; + } + + public componentWillUnmount() { + SettingsStore.unwatchSetting(this.countWatcherRef); } public componentDidUpdate(prevProps: Readonly) { @@ -75,24 +103,34 @@ export default class NotificationBadge extends React.PureComponent { + this.setState({showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId)}); + }; + private onNotificationUpdate = () => { this.forceUpdate(); // notification state changed - update }; public render(): React.ReactElement { // Don't show a badge if we don't need to - if (this.props.notification.color <= NotificationColor.Bold) return null; + if (this.props.notification.color <= NotificationColor.None) return null; const hasNotif = this.props.notification.color >= NotificationColor.Red; const hasCount = this.props.notification.color >= NotificationColor.Grey; - const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount"); + const hasUnread = this.props.notification.color >= NotificationColor.Bold; + const couldBeEmpty = !this.state.showCounts || hasUnread; + let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); + if (this.props.forceCount) { + isEmptyBadge = false; + if (!hasCount) return null; // Can't render a badge + } let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count); if (isEmptyBadge) symbol = ""; const classes = classNames({ 'mx_NotificationBadge': true, - 'mx_NotificationBadge_visible': hasCount, + 'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount, 'mx_NotificationBadge_highlighted': hasNotif, 'mx_NotificationBadge_dot': isEmptyBadge, 'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3, diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 08a41570e3..562c307769 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -267,7 +267,7 @@ export default class RoomSublist2 extends React.Component { // TODO: Collapsed state - const badge = ; + const badge = ; let addRoomButton = null; if (!!this.props.onAddRoom) { diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 9f4870d437..7f91b5ee9d 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -248,7 +248,13 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ; + const badge = ( + + ); // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index ca8647e067..7cf0e629ec 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -188,6 +188,11 @@ export const SETTINGS = { default: true, invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', }, + // TODO: Wire up appropriately to UI (FTUE notifications) + "Notifications.alwaysShowBadgeCounts": { + supportedLevels: LEVELS_ROOM_OR_ACCOUNT, + default: false, + }, "useCompactLayout": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Use compact timeline layout'), From 8201eed929025bc24ae341d74fef4a5e494883ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 16:03:38 -0600 Subject: [PATCH 017/106] Encourage counts if the user has a mention (red state) --- src/components/views/rooms/NotificationBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 65ccc88c5f..2ddf095b59 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -118,7 +118,7 @@ export default class NotificationBadge extends React.PureComponent= NotificationColor.Red; const hasCount = this.props.notification.color >= NotificationColor.Grey; const hasUnread = this.props.notification.color >= NotificationColor.Bold; - const couldBeEmpty = !this.state.showCounts || hasUnread; + const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif; let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); if (this.props.forceCount) { isEmptyBadge = false; From 241e0c12f0c9203b2ebb706d2ac8bc57b587f54d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:45:48 -0600 Subject: [PATCH 018/106] Trigger room-specific watchers whenever a higher level change happens Otherwise the room list badges end up having to listen to `null` for a room ID, meaning they have to filter. The notification badge count setting is the first ever setting to watch based on a room ID, so there are no compatibility concerns with this change. --- src/settings/WatchManager.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/settings/WatchManager.js b/src/settings/WatchManager.js index 472b13966f..3f54ca929e 100644 --- a/src/settings/WatchManager.js +++ b/src/settings/WatchManager.js @@ -51,8 +51,17 @@ export class WatchManager { const roomWatchers = this._watchers[settingName]; const callbacks = []; - if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]); - if (roomWatchers[null]) callbacks.push(...roomWatchers[null]); + if (inRoomId !== null && roomWatchers[inRoomId]) { + callbacks.push(...roomWatchers[inRoomId]); + } + + if (!inRoomId) { + // Fire updates to all the individual room watchers too, as they probably + // care about the change higher up. + callbacks.push(...Object.values(roomWatchers).reduce((r, a) => [...r, ...a], [])); + } else if (roomWatchers[null]) { + callbacks.push(...roomWatchers[null]); + } for (const callback of callbacks) { callback(inRoomId, atLevel, newValueAtLevel); From 63ad14ae1e7a06a456b6b43a0e11bf803d3c3049 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:35:55 -0600 Subject: [PATCH 019/106] Clean up imports --- src/components/views/rooms/NotificationBadge.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2ddf095b59..36c269beba 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -18,17 +18,11 @@ import React from "react"; import classNames from "classnames"; import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../views/elements/AccessibleButton"; -import RoomAvatar from "../../views/avatars/RoomAvatar"; -import dis from '../../../dispatcher/dispatcher'; -import { Key } from "../../../Keyboard"; import * as RoomNotifs from '../../../RoomNotifs'; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; import * as Unread from '../../../Unread'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; From fe65b7631dfd9b985e1e1f852a4e795dca617fd9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:57:08 -0600 Subject: [PATCH 020/106] Soften warning about lack of rooms in setting updates --- src/settings/handlers/RoomSettingsHandler.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 4116f26220..2ed82b577d 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -42,9 +42,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl _onEvent(event, state, prevEvent) { const roomId = event.getRoomId(); const room = this.client.getRoom(roomId); - if (!room) throw new Error(`Unknown room caused state update: ${roomId}`); - if (state !== room.currentState) return; // ignore state updates which are not current + // Note: the tests often fire setting updates that don't have rooms in the store, so + // we fail softly here. We shouldn't assume that the state being fired is current + // state, but we also don't need to explode just because we didn't find a room. + if (!room) console.warn(`Unknown room caused setting update: ${roomId}`); + if (room && state !== room.currentState) return; // ignore state updates which are not current if (event.getType() === "org.matrix.room.preview_urls") { let val = event.getContent()['disable']; From 784e73831bb070237e63ba2e75d527ac5b071a01 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 11:23:38 -0600 Subject: [PATCH 021/106] Move setting to account only (no per-room) --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7cf0e629ec..028f355ab8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -190,7 +190,7 @@ export const SETTINGS = { }, // TODO: Wire up appropriately to UI (FTUE notifications) "Notifications.alwaysShowBadgeCounts": { - supportedLevels: LEVELS_ROOM_OR_ACCOUNT, + supportedLevels: ['account'], default: false, }, "useCompactLayout": { From 9e3c101172c1f14c827de53d0d548ba79dbff6c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 11:24:04 -0600 Subject: [PATCH 022/106] Clone reads of account data to prevent mutation --- src/settings/handlers/AccountSettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 7b78c39c3a..c396a9d4de 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -162,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const event = cli.getAccountData(eventType); if (!event || !event.getContent()) return null; - return event.getContent(); + return JSON.parse(JSON.stringify(event.getContent())); // clone to prevent mutation } _notifyBreadcrumbsUpdate(event) { From 38bf1680a242ea8d50176791354a58326db54adc Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 22 Jun 2020 19:02:03 +0100 Subject: [PATCH 023/106] Update read receipt remainder for internal font size change In https://github.com/matrix-org/matrix-react-sdk/pull/4725, we changed the internal font size from 15 to 10, but the `toRem` function (currently only used for read receipts remainders curiously) was not updated. This updates the function, which restores the remainders. Fixes https://github.com/vector-im/riot-web/issues/14127 --- src/utils/units.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/units.ts b/src/utils/units.ts index 54dd6b0523..03775f4c21 100644 --- a/src/utils/units.ts +++ b/src/utils/units.ts @@ -19,7 +19,7 @@ limitations under the License. // converts a pixel value to rem. export function toRem(pixelValue: number): string { - return pixelValue / 15 + "rem"; + return pixelValue / 10 + "rem"; } export function toPx(pixelValue: number): string { From cf92fc37d42c10616835124543790aad1ff9b84d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 12:51:53 -0600 Subject: [PATCH 024/106] Fix layout of minimized view for new room list --- res/css/views/rooms/_RoomSublist2.scss | 8 ++++---- src/components/views/rooms/RoomSublist2.tsx | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 66615fb6a8..24151b15b0 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -304,18 +304,18 @@ limitations under the License. position: relative; .mx_RoomSublist2_badgeContainer { - order: 1; + order: 0; align-self: flex-end; margin-right: 0; } - .mx_RoomSublist2_headerText { - order: 2; + .mx_RoomSublist2_stickable { + order: 1; max-width: 100%; } .mx_RoomSublist2_auxButton { - order: 4; + order: 2; visibility: visible; width: 32px !important; // !important to override hover styles height: 32px !important; // !important to override hover styles diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 08a41570e3..4a145ede11 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -291,7 +291,18 @@ export default class RoomSublist2 extends React.Component { 'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, }); + const badgeContainer = ( +
+ {badge} +
+ ); + // TODO: a11y (see old component) + // Note: the addRoomButton conditionally gets moved around + // the DOM depending on whether or not the list is minimized. + // If we're minimized, we want it below the header so it + // doesn't become sticky. + // The same applies to the notification badge. return (
@@ -307,11 +318,11 @@ export default class RoomSublist2 extends React.Component { {this.props.label} {this.renderMenu()} - {addRoomButton} -
- {badge} -
+ {this.props.isMinimized ? null : addRoomButton} + {this.props.isMinimized ? null : badgeContainer}
+ {this.props.isMinimized ? badgeContainer : null} + {this.props.isMinimized ? addRoomButton : null}
); }} From 1a427b8ff78522f48d9ca885d277da68b4010363 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 13:09:42 -0600 Subject: [PATCH 025/106] Fix sticky headers over/under extending themselves in the new room list Fixes https://github.com/vector-im/riot-web/issues/14095 --- src/components/structures/LeftPanel2.tsx | 35 +++++++++++++++++----- src/components/structures/LoggedInView.tsx | 5 +++- src/utils/ResizeNotifier.js | 21 ++++++++++--- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index b5da44caef..378a24a70e 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -19,7 +19,6 @@ import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; -import SearchBox from "./SearchBox"; import RoomList2 from "../views/rooms/RoomList2"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -30,6 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import { createRef } from "react"; /******************************************************************* * CAUTION * @@ -41,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; interface IProps { isMinimized: boolean; + resizeNotifier: ResizeNotifier; } interface IState { @@ -49,6 +51,8 @@ interface IState { } export default class LeftPanel2 extends React.Component { + private listContainerRef: React.RefObject = createRef(); + // TODO: Properly support TagPanel // TODO: Properly support searching/filtering // TODO: Properly support breadcrumbs @@ -65,10 +69,15 @@ export default class LeftPanel2 extends React.Component { }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + + // We watch the middle panel because we don't actually get resized, the middle panel does. + // We listen to the noisy channel to avoid choppy reaction times. + this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } private onSearch = (term: string): void => { @@ -86,9 +95,7 @@ export default class LeftPanel2 extends React.Component { } }; - // TODO: Apply this on resize, init, etc for reliability - private onScroll = (ev: React.MouseEvent) => { - const list = ev.target as HTMLDivElement; + private handleStickyHeaders(list: HTMLDivElement) { const rlRect = list.getBoundingClientRect(); const bottom = rlRect.bottom; const top = rlRect.top; @@ -123,6 +130,18 @@ export default class LeftPanel2 extends React.Component { header.style.top = `unset`; } } + } + + // TODO: Apply this on resize, init, etc for reliability + private onScroll = (ev: React.MouseEvent) => { + const list = ev.target as HTMLDivElement; + this.handleStickyHeaders(list); + }; + + private onResize = () => { + console.log("Resize width"); + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); }; private renderHeader(): React.ReactNode { @@ -230,9 +249,11 @@ export default class LeftPanel2 extends React.Component {
); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index f37f77b31b..1bc656e6a3 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -677,7 +677,10 @@ class LoggedInView extends React.PureComponent { if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { // TODO: Supply props like collapsed and disabled to LeftPanel2 leftPanel = ( - + ); } diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index d65bc4bd07..f726a43e08 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -15,9 +15,13 @@ limitations under the License. */ /** - * Fires when the middle panel has been resized. + * Fires when the middle panel has been resized (throttled). * @event module:utils~ResizeNotifier#"middlePanelResized" */ +/** + * Fires when the middle panel has been resized by a pixel. + * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" + */ import { EventEmitter } from "events"; import { throttle } from "lodash"; @@ -29,15 +33,24 @@ export default class ResizeNotifier extends EventEmitter { this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); } + _noisyMiddlePanel() { + this.emit("middlePanelResizedNoisy"); + } + + _updateMiddlePanel() { + this._throttledMiddlePanel(); + this._noisyMiddlePanel(); + } + // can be called in quick succession notifyLeftHandleResized() { // don't emit event for own region - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } // can be called in quick succession notifyRightHandleResized() { - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } // can be called in quick succession @@ -48,7 +61,7 @@ export default class ResizeNotifier extends EventEmitter { // taller than the available space this.emit("leftPanelResized"); - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } } From 6c48966bf5417cacc4c50cf2f7bee779371d576c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 13:34:49 -0600 Subject: [PATCH 026/106] Have the theme switcher set the device-level theme to match settings Fixes https://github.com/vector-im/riot-web/issues/14111 This is a shortcut into the Appearance tab, so use the same level. It was an explicit decision to have the tab set the theme at the device level. --- src/components/structures/UserMenuButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 6607fffdd1..04b1b03368 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -117,7 +117,7 @@ export default class UserMenuButton extends React.Component { SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); const newTheme = this.state.isDarkTheme ? "light" : "dark"; - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab }; private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { From 1fe3e33dbf944cc0f078c18c253d019523af540d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 14:14:43 -0600 Subject: [PATCH 027/106] Factor out cloning to a util and use it everywhere --- src/FromWidgetPostMessageApi.js | 5 +++-- src/ScalarMessaging.js | 5 +++-- src/components/views/terms/InlineTermsAgreement.js | 3 ++- src/settings/handlers/AccountSettingsHandler.js | 4 ++-- src/settings/handlers/RoomAccountSettingsHandler.js | 4 ++-- src/settings/handlers/RoomSettingsHandler.js | 4 ++-- src/utils/WidgetUtils.js | 3 ++- src/utils/objects.ts | 11 +++++++++++ src/widgets/WidgetApi.ts | 3 ++- 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 102afa6bf1..1b4aa19ebf 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -25,6 +25,7 @@ import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; import {Capability} from "./widgets/WidgetApi"; +import {objectClone} from "./utils/objects"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -247,7 +248,7 @@ export default class FromWidgetPostMessageApi { * @param {Object} res Response data */ sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = res; event.source.postMessage(data, event.origin); } @@ -260,7 +261,7 @@ export default class FromWidgetPostMessageApi { */ sendError(event, msg, nestedError) { console.error('Action:' + event.data.action + ' failed with message: ' + msg); - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = { error: { message: msg, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 315c2d86f4..b33aa57359 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -244,16 +244,17 @@ import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {WidgetType} from "./widgets/WidgetType"; +import {objectClone} from "./utils/objects"; function sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = res; event.source.postMessage(data, event.origin); } function sendError(event, msg, nestedError) { console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = { error: { message: msg, diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index bccd686cd3..55719fe57f 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -18,6 +18,7 @@ import React from "react"; import PropTypes from "prop-types"; import {_t, pickBestLanguage} from "../../../languageHandler"; import * as sdk from "../../.."; +import {objectClone} from "../../../utils/objects"; export default class InlineTermsAgreement extends React.Component { static propTypes = { @@ -56,7 +57,7 @@ export default class InlineTermsAgreement extends React.Component { } _togglePolicy = (index) => { - const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone + const policies = objectClone(this.state.policies); policies[index].checked = !policies[index].checked; this.setState({policies}); }; diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index c396a9d4de..732ce6c550 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; @@ -162,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const event = cli.getAccountData(eventType); if (!event || !event.getContent()) return null; - return JSON.parse(JSON.stringify(event.getContent())); // clone to prevent mutation + return objectClone(event.getContent()); // clone to prevent mutation } _notifyBreadcrumbsUpdate(event) { diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 1c818cef71..b2af81779b 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; @@ -137,6 +137,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin const event = room.getAccountData(eventType); if (!event || !event.getContent()) return null; - return event.getContent(); + return objectClone(event.getContent()); // clone to prevent mutation } } diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 2ed82b577d..d8e775742c 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; /** * Gets and sets settings at the "room" level. @@ -117,6 +117,6 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl const event = room.currentState.getStateEvents(eventType, ""); if (!event || !event.getContent()) return null; - return event.getContent(); + return objectClone(event.getContent()); // clone to prevent mutation } } diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index b48ec481ba..f7f4be202b 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -31,6 +31,7 @@ import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {Capability} from "../widgets/WidgetApi"; import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; +import {objectClone} from "./objects"; export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -222,7 +223,7 @@ export default class WidgetUtils { const client = MatrixClientPeg.get(); // Get the current widgets and clone them before we modify them, otherwise // we'll modify the content of the old event. - const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets())); + const userWidgets = objectClone(WidgetUtils.getUserWidgets()); // Delete existing widget with ID try { diff --git a/src/utils/objects.ts b/src/utils/objects.ts index cbb311cc48..14fa928ce2 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -47,3 +47,14 @@ export function objectKeyChanges(a: any, b: any): string[] { const diff = objectDiff(a, b); return arrayMerge(diff.removed, diff.added, diff.changed); } + +/** + * Clones an object by running it through JSON parsing. Note that this + * will destroy any complicated object types which do not translate to + * JSON. + * @param obj The object to clone. + * @returns The cloned object + */ +export function objectClone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 795c6648ef..d5f2f2258e 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -19,6 +19,7 @@ limitations under the License. import { randomString } from "matrix-js-sdk/src/randomstring"; import { EventEmitter } from "events"; +import { objectClone } from "../utils/objects"; export enum Capability { Screenshot = "m.capability.screenshot", @@ -140,7 +141,7 @@ export class WidgetApi extends EventEmitter { private replyToRequest(payload: ToWidgetRequest, reply: any) { if (!window.parent) return; - const request = JSON.parse(JSON.stringify(payload)); + const request = objectClone(payload); request.response = reply; window.parent.postMessage(request, this.origin); From fb551781c20dc7f872e866b615126d1fd1b70a55 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 14:52:17 -0600 Subject: [PATCH 028/106] Force DMs to always be red notifications This also passes the tagId to the sublist so it doesn't have to rip it out of the `layout`. It doesn't get a layout until later anyways, which causes some null issues. --- .../views/rooms/NotificationBadge.tsx | 40 +++++++++++++++++-- src/components/views/rooms/RoomList2.tsx | 1 + src/components/views/rooms/RoomSublist2.tsx | 20 +++++----- src/components/views/rooms/RoomTile2.tsx | 8 +++- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 36c269beba..37b61714b9 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -27,6 +27,7 @@ import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -139,7 +140,7 @@ export default class NotificationBadge extends React.PureComponent { components.push( { super(props); this.state = { - notificationState: new ListNotificationState(this.props.isInvite), + notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId), menuDisplayed: false, }; this.state.notificationState.setRooms(this.props.rooms); @@ -130,13 +132,13 @@ export default class RoomSublist2 extends React.Component { }; private onUnreadFirstChanged = async () => { - const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance; - await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm); + await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm); }; private onTagSortChanged = async (sort: SortAlgorithm) => { - await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort); + await RoomListStore.instance.setTagSorting(this.props.tagId, sort); }; private onMessagePreviewChanged = () => { @@ -176,7 +178,7 @@ export default class RoomSublist2 extends React.Component { key={`room-${room.roomId}`} showMessagePreview={this.props.layout.showPreviews} isMinimized={this.props.isMinimized} - tag={this.props.layout.tagId} + tag={this.props.tagId} /> ); } @@ -189,8 +191,8 @@ export default class RoomSublist2 extends React.Component { let contextMenu = null; if (this.state.menuDisplayed) { const elementRect = this.menuButtonRef.current.getBoundingClientRect(); - const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic; - const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; + const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; contextMenu = ( { this.onTagSortChanged(SortAlgorithm.Recent)} checked={!isAlphabetical} - name={`mx_${this.props.layout.tagId}_sortBy`} + name={`mx_${this.props.tagId}_sortBy`} > {_t("Activity")} this.onTagSortChanged(SortAlgorithm.Alphabetic)} checked={isAlphabetical} - name={`mx_${this.props.layout.tagId}_sortBy`} + name={`mx_${this.props.tagId}_sortBy`} > {_t("A-Z")} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 7f91b5ee9d..18b4ee8185 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,7 +26,11 @@ import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; -import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge"; +import NotificationBadge, { + INotificationState, + NotificationColor, + TagSpecificNotificationState +} from "./NotificationBadge"; import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; @@ -79,7 +83,7 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, - notificationState: new RoomNotificationState(this.props.room), + notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, generalMenuDisplayed: false, }; From fc5ee64fce2f14a7dfcf971effde6ddb0beb9e5a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 15:12:30 -0600 Subject: [PATCH 029/106] Fix read receipt handling in the new room list Fixes https://github.com/vector-im/riot-web/issues/14064 Fixes https://github.com/vector-im/riot-web/issues/14082 Turns out the event doesn't reference a room, so we need to use the accompanied room reference instead. --- src/components/views/rooms/NotificationBadge.tsx | 11 +++++++++-- src/stores/room-list/RoomListStore2.ts | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 37b61714b9..523b5a55cc 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -28,6 +28,7 @@ import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { readReceiptChangeIsFor } from "../../../utils/read-receipts"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -147,7 +148,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, constructor(private room: Room) { super(); - this.room.on("Room.receipt", this.handleRoomEventUpdate); + this.room.on("Room.receipt", this.handleReadReceipt); this.room.on("Room.timeline", this.handleRoomEventUpdate); this.room.on("Room.redaction", this.handleRoomEventUpdate); MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); @@ -171,7 +172,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, } public destroy(): void { - this.room.removeListener("Room.receipt", this.handleRoomEventUpdate); + this.room.removeListener("Room.receipt", this.handleReadReceipt); this.room.removeListener("Room.timeline", this.handleRoomEventUpdate); this.room.removeListener("Room.redaction", this.handleRoomEventUpdate); if (MatrixClientPeg.get()) { @@ -179,6 +180,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, } } + private handleReadReceipt = (event: MatrixEvent, room: Room) => { + if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore + if (room.roomId !== this.room.roomId) return; // not for us - ignore + this.updateNotificationState(); + }; + private handleRoomEventUpdate = (event: MatrixEvent) => { const roomId = event.getRoomId(); diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 9684e338f8..99eee82d4e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -158,12 +158,12 @@ export class RoomListStore2 extends AsyncStore { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). if (readReceiptChangeIsFor(payload.event, this.matrixClient)) { - console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`); - const room = this.matrixClient.getRoom(payload.event.roomId); + const room = payload.room; if (!room) { - console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`); + console.warn(`Own read receipt was in unknown room ${room.roomId}`); return; } + console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`); await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt); return; } From 115c850d35b64491bb3170374bc8d91f332c9e88 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 15:57:13 -0600 Subject: [PATCH 030/106] Use the correct timeline reference for message previews Fixes https://github.com/vector-im/riot-web/issues/14083 (hopefully) This is the same logic used by `Unread.js`, so should be correct. --- src/stores/MessagePreviewStore.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/MessagePreviewStore.ts index 64d65a72f3..9e225d1709 100644 --- a/src/stores/MessagePreviewStore.ts +++ b/src/stores/MessagePreviewStore.ts @@ -78,9 +78,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } private generatePreview(room: Room) { - const timeline = room.getLiveTimeline(); - if (!timeline) return; // usually only happens in tests - const events = timeline.getEvents(); + const events = room.timeline; + if (!events) return; // should only happen in tests for (let i = events.length - 1; i >= 0; i--) { if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached From 680e997a94ff7b34ccfa6638f6bd8b33fe8b8896 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 23 Jun 2020 13:38:50 +0100 Subject: [PATCH 031/106] Cleanup tooltip classnames --- src/components/views/elements/Field.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 9d53576259..834edff7df 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -244,9 +244,8 @@ export default class Field extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = ; From 54ba75afd7e8ebc6809c7dcb2c678ee43bbece7b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 14:48:23 +0100 Subject: [PATCH 032/106] Upgrade matrix-js-sdk to 7.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 06c4c43622..07cb11d45b 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "7.0.0-rc.1", + "matrix-js-sdk": "7.0.0", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 2bb99f4602..2aa39a257f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,10 +5820,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@7.0.0-rc.1: - version "7.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0-rc.1.tgz#95a258218f5c5ec73ec4be510b28768c35809a0b" - integrity sha512-1znl0d2UxU6Mmimy+pMSQP1lQfsmDb9jxiKV5sfMvTBsLtUE2cTqEBVDNVoOHL4UJ9U4oMLsrBgu3sELkgSJLQ== +matrix-js-sdk@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0.tgz#da2b24e57574379c3d8f7065eb68ea6c479d9806" + integrity sha512-0i1NmfwS5HzEPPjqUAXpw81o+8DImBS67QQBemJiM6D/imU3KFBacdhkmgjMLKXnAfTy6a+aCGfYBfVolfmNQw== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 059d5f927d549a6ebb22e6adbbbd3144814a3b31 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:06:52 +0100 Subject: [PATCH 033/106] Prepare changelog for v2.8.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7a73b264..21bfbf0a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0) (2020-06-23) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) + + * Update read receipt remainder for internal font size change + [\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807) + * Revert "Use recovery keys over passphrases" + [\#4793](https://github.com/matrix-org/matrix-react-sdk/pull/4793) + Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1) From 5256a86545d601a4579024f5650ce16ddc2d2f87 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:06:53 +0100 Subject: [PATCH 034/106] v2.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07cb11d45b..591922498f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.8.0-rc.1", + "version": "2.8.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From f3af6031028f0718450bb10d8e067e4504667674 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:19:55 +0100 Subject: [PATCH 035/106] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 591922498f..673ea34a68 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "7.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 2aa39a257f..c20658f014 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,10 +5820,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@7.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "7.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0.tgz#da2b24e57574379c3d8f7065eb68ea6c479d9806" - integrity sha512-0i1NmfwS5HzEPPjqUAXpw81o+8DImBS67QQBemJiM6D/imU3KFBacdhkmgjMLKXnAfTy6a+aCGfYBfVolfmNQw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f683f4544aa5da150836b01c754062809119fa97" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 38293627545e8ab1b7f4245e94bcd377f298ca10 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 23 Jun 2020 15:24:02 +0100 Subject: [PATCH 036/106] Fix up merge to develop --- src/components/structures/auth/SetupEncryptionBody.js | 8 ++++++++ src/stores/SetupEncryptionStore.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 6b30f3c084..f2e702c8cb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -28,6 +28,14 @@ import { PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; +function keyHasPassphrase(keyInfo) { + return ( + keyInfo.passphrase && + keyInfo.passphrase.salt && + keyInfo.passphrase.iterations + ); +} + export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 2badf3bc33..63b8c428eb 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -36,7 +36,7 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_INTRO; + this.phase = PHASE_BUSY; this.verificationRequest = null; this.backupInfo = null; From 6739c90f442450f4cf036e777bb66cbbe32cbe0b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:25:59 +0100 Subject: [PATCH 037/106] Fix SDK in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21bfbf0a93..964e75f7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) + * Upgrade to JS SDK 7.0.0 * Update read receipt remainder for internal font size change [\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807) * Revert "Use recovery keys over passphrases" From 86597aabca88b538e4ec94cab3166704895f8567 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:52:54 +0200 Subject: [PATCH 038/106] better naming --- src/theme.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/theme.js b/src/theme.js index 22cfd8b076..095990de3a 100644 --- a/src/theme.js +++ b/src/theme.js @@ -39,7 +39,7 @@ export function enumerateThemes() { function setCustomThemeVars(customTheme) { const {style} = document.body; - function setCSSVariable(name, hexColor, doPct = true) { + function setCSSColorVariable(name, hexColor, doPct = true) { style.setProperty(`--${name}`, hexColor); if (doPct) { // uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50% @@ -53,10 +53,10 @@ function setCustomThemeVars(customTheme) { for (const [name, value] of Object.entries(customTheme.colors)) { if (Array.isArray(value)) { for (let i = 0; i < value.length; i += 1) { - setCSSVariable(`${name}_${i}`, value[i], false); + setCSSColorVariable(`${name}_${i}`, value[i], false); } } else { - setCSSVariable(name, value); + setCSSColorVariable(name, value); } } } From e083856801179eef1c6efb6452ea50b46d3d8f7e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:53:16 +0200 Subject: [PATCH 039/106] allow changing the font-family --- res/themes/light-custom/css/_custom.scss | 2 ++ src/theme.js | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 6206496150..e7912e3cb0 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$font-family: var(--font-family, $font-family); +$monospace-font-family: var(--font-family-monospace, $monospace-font-family); // // --accent-color $accent-color: var(--accent-color); diff --git a/src/theme.js b/src/theme.js index 095990de3a..da70dc7bb1 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,6 +60,15 @@ function setCustomThemeVars(customTheme) { } } } + if (customTheme.fonts) { + const {fonts} = customTheme; + if (fonts.general) { + style.setProperty("--font-family", fonts.general); + } + if (fonts.monospace) { + style.setProperty("--font-family-monospace", fonts.monospace); + } + } } export function getCustomTheme(themeName) { From b3510aa2b47c3609810cbce2e4b2115b97291d0f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:17 +0200 Subject: [PATCH 040/106] remove css vars when switching theme --- src/theme.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/theme.js b/src/theme.js index da70dc7bb1..7e0eaabf11 100644 --- a/src/theme.js +++ b/src/theme.js @@ -35,6 +35,16 @@ export function enumerateThemes() { return Object.assign({}, customThemeNames, BUILTIN_THEMES); } +function clearCustomTheme() { + // remove all css variables, we assume these are there because of the custom theme + const inlineStyleProps = Object.values(document.body.style); + for (const prop of inlineStyleProps) { + if (prop.startsWith("--")) { + document.body.style.removeProperty(prop); + } + } +} + function setCustomThemeVars(customTheme) { const {style} = document.body; @@ -97,6 +107,7 @@ export async function setTheme(theme) { const themeWatcher = new ThemeWatcher(); theme = themeWatcher.getEffectiveTheme(); } + clearCustomTheme(); let stylesheetName = theme; if (theme.startsWith("custom-")) { const customTheme = getCustomTheme(theme.substr(7)); From 2f6fc6bba24140367c5ecdd18d40d611251d9997 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:38 +0200 Subject: [PATCH 041/106] allow adding custom font faces in theme --- src/theme.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/theme.js b/src/theme.js index 7e0eaabf11..d560cc96c8 100644 --- a/src/theme.js +++ b/src/theme.js @@ -43,8 +43,23 @@ function clearCustomTheme() { document.body.style.removeProperty(prop); } } + const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']"); + if (customFontFaceStyle) { + customFontFaceStyle.remove(); + } } +function generateCustomFontFaceCSS(faces) { + return Object.entries(faces).map(([fontName, face]) => { + const src = Object.entries(face.src).map(([format, url]) => { + return `url('${url}') format('${format}')`; + }).join(", "); + return `@font-face {` + + ` font-family: '${fontName}';` + + ` src: ${src};` + + `}`; + }).join("\n"); +} function setCustomThemeVars(customTheme) { const {style} = document.body; @@ -72,6 +87,14 @@ function setCustomThemeVars(customTheme) { } if (customTheme.fonts) { const {fonts} = customTheme; + if (fonts.faces) { + const css = generateCustomFontFaceCSS(fonts.faces); + const style = document.createElement("style"); + style.setAttribute("title", "custom-theme-font-faces"); + style.setAttribute("type", "text/css"); + style.appendChild(document.createTextNode(css)); + document.head.appendChild(style); + } if (fonts.general) { style.setProperty("--font-family", fonts.general); } From 3b13a623cd755af6b196558d95515c428b8a1d83 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:57 +0200 Subject: [PATCH 042/106] cleanup --- src/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.js b/src/theme.js index d560cc96c8..ee9cb9c7db 100644 --- a/src/theme.js +++ b/src/theme.js @@ -179,7 +179,7 @@ export async function setTheme(theme) { if (a == styleElements[stylesheetName]) return; a.disabled = true; }); - const bodyStyles = global.getComputedStyle(document.getElementsByTagName("body")[0]); + const bodyStyles = global.getComputedStyle(document.body); if (bodyStyles.backgroundColor) { document.querySelector('meta[name="theme-color"]').content = bodyStyles.backgroundColor; } From dd9112a01a926b39f363c49c0a384b9f6e450826 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 11:44:40 -0600 Subject: [PATCH 043/106] Decrease margin between new sublists This is an attempt to increase density without adjusting the tiles directly. --- res/css/views/rooms/_RoomSublist2.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 24151b15b0..c7dae56353 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -22,10 +22,12 @@ limitations under the License. flex-direction: column; margin-left: 8px; - margin-top: 12px; - margin-bottom: 12px; width: 100%; + &:first-child { + margin-top: 12px; // so we're not up against the search/filter + } + .mx_RoomSublist2_headerContainer { // Create a flexbox to make alignment easy display: flex; From f93d67fc65c9914286431b384020176c29b31e1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 16:49:39 -0600 Subject: [PATCH 044/106] Fix sticky room disappearing/jumping in search results Fixes https://github.com/vector-im/riot-web/issues/14124 Fixes https://github.com/vector-im/riot-web/issues/14154 (which was technically supposed to say that the sticky room when filtering was always last) This is all a bit complicated, but the theory is that we end up with a stable list even through filtering. There's some notes within, though I suspect it'll be difficult to understand :( --- src/stores/room-list/algorithms/Algorithm.ts | 47 +++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 052c58bb83..9eb0d27748 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -170,12 +170,16 @@ export class Algorithm extends EventEmitter { // When we do have a room though, we expect to be able to find it const tag = this.roomIdsToTags[val.roomId][0]; if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`); - let position = this.cachedRooms[tag].indexOf(val); + + // We specifically do NOT use the ordered rooms set as it contains the sticky room, which + // means we'll be off by 1 when the user is switching rooms. This leads to visual jumping + // when the user is moving south in the list (not north, because of math). + let position = this.getOrderedRoomsWithoutSticky()[tag].indexOf(val); if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`); // 🐉 Here be dragons. // Before we can go through with lying to the underlying algorithm about a room - // we need to ensure that when we do we're ready for the innevitable sticky room + // we need to ensure that when we do we're ready for the inevitable sticky room // update we'll receive. To prepare for that, we first remove the sticky room and // recalculate the state ourselves so that when the underlying algorithm calls for // the same thing it no-ops. After we're done calling the algorithm, we'll issue @@ -208,6 +212,12 @@ export class Algorithm extends EventEmitter { position: position, tag: tag, }; + + // We update the filtered rooms just in case, as otherwise users will end up visiting + // a room while filtering and it'll disappear. We don't update the filter earlier in + // this function simply because we don't have to. + this.recalculateFilteredRoomsForTag(tag); + if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(tag); this.recalculateStickyRoom(); // Finally, trigger an update @@ -231,9 +241,7 @@ export class Algorithm extends EventEmitter { // We optimize our lookups by trying to reduce sample size as much as possible // to the rooms we know will be deduped by the Set. const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone - if (this._stickyRoom && this._stickyRoom.tag === tagId && this._stickyRoom.room) { - rooms.push(this._stickyRoom.room); - } + this.tryInsertStickyRoomToFilterSet(rooms, tagId); let remainingRooms = rooms.map(r => r); let allowedRoomsInThisTag = []; let lastFilterPriority = orderedFilters[0].relativePriority; @@ -263,6 +271,7 @@ export class Algorithm extends EventEmitter { this.emit(LIST_UPDATED_EVENT); } + // TODO: Remove or use. protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void { const filters = this.allowedByFilter.keys(); for (const room of added) { @@ -281,7 +290,8 @@ export class Algorithm extends EventEmitter { protected recalculateFilteredRoomsForTag(tagId: TagID): void { console.log(`Recalculating filtered rooms for ${tagId}`); delete this.filteredRooms[tagId]; - const rooms = this.cachedRooms[tagId]; + const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone + this.tryInsertStickyRoomToFilterSet(rooms, tagId); const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r)); if (filteredRooms.length > 0) { this.filteredRooms[tagId] = filteredRooms; @@ -289,6 +299,17 @@ export class Algorithm extends EventEmitter { console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); } + protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) { + if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return; + + const position = this._stickyRoom.position; + if (position >= rooms.length) { + rooms.push(this._stickyRoom.room); + } else { + rooms.splice(position, 0, this._stickyRoom.room); + } + } + /** * Recalculate the sticky room position. If this is being called in relation to * a specific tag being updated, it should be given to this function to optimize @@ -377,6 +398,20 @@ export class Algorithm extends EventEmitter { return this.filteredRooms; } + /** + * This returns the same as getOrderedRooms(), but without the sticky room + * map as it causes issues for sticky room handling (see sticky room handling + * for more information). + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + private getOrderedRoomsWithoutSticky(): ITagMap { + if (!this.hasFilters) { + return this.cachedRooms; + } + return this.filteredRooms; + } + /** * Seeds the Algorithm with a set of rooms. The algorithm will discard all * previously known information and instead use these rooms instead. From 380aed4244a7b83183247ecf3a7f710ef28e9653 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 20:59:26 -0600 Subject: [PATCH 045/106] Update profile information in User Menu Fixes https://github.com/vector-im/riot-web/issues/14158 (we needed an HTTP avatar URL) Fixes https://github.com/vector-im/riot-web/issues/14159 Fixes https://github.com/vector-im/riot-web/issues/14157 Also fixes an issue where it wasn't updating automatically when the user changed their profile info. This is all achieved through a new OwnProfileStore which does the heavy lifting, as we have to keep at least 2 components updated. --- res/css/structures/_LeftPanel2.scss | 9 ++ res/css/structures/_UserMenuButton.scss | 3 - src/components/structures/LeftPanel2.tsx | 41 ++++--- src/components/structures/UserMenuButton.tsx | 24 ++-- src/i18n/strings/en_EN.json | 2 +- src/stores/OwnProfileStore.ts | 122 +++++++++++++++++++ 6 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 src/stores/OwnProfileStore.ts diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 5cdefa0324..dd28a3107c 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -65,6 +65,10 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_userAvatarContainer { position: relative; // to make default avatars work margin-right: 8px; + + .mx_LeftPanel2_userAvatar { + border-radius: 32px; // should match avatar size + } } .mx_LeftPanel2_userName { @@ -72,6 +76,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations font-size: $font-15px; line-height: $font-20px; flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .mx_LeftPanel2_headerButtons { diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 1fbbbb5fd8..3871fc22ef 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -35,9 +35,6 @@ limitations under the License. // Create another flexbox of columns to handle large user IDs display: flex; flex-direction: column; - - // fit the container - flex: 1; width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button * { diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 378a24a70e..ec846bd177 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import * as React from "react"; +import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; @@ -30,7 +31,9 @@ import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { createRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { throttle } from 'lodash'; +import { OwnProfileStore } from "../../stores/OwnProfileStore"; /******************************************************************* * CAUTION * @@ -73,13 +76,32 @@ export default class LeftPanel2 extends React.Component { // We watch the middle panel because we don't actually get resized, the middle panel does. // We listen to the noisy channel to avoid choppy reaction times. this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } + // TSLint wants this to be a member, but we don't want that. + // tslint:disable-next-line + private onRoomStateUpdate = throttle((ev: MatrixEvent) => { + const myUserId = MatrixClientPeg.get().getUserId(); + if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { + // noinspection JSIgnoredPromiseFromCall + this.onProfileUpdate(); + } + }, 200, {trailing: true, leading: true}); + + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + private onSearch = (term: string): void => { this.setState({searchFilter: term}); }; @@ -149,16 +171,7 @@ export default class LeftPanel2 extends React.Component { // TODO: Presence // TODO: Breadcrumbs toggle // TODO: Menu button - const avatarSize = 32; - // TODO: Don't do this profile lookup in render() - const client = MatrixClientPeg.get(); - let displayName = client.getUserId(); - let avatarUrl: string = null; - const myUser = client.getUser(client.getUserId()); - if (myUser) { - displayName = myUser.rawDisplayName; - avatarUrl = myUser.avatarUrl; - } + const avatarSize = 32; // should match border-radius of the avatar let breadcrumbs; if (this.state.showBreadcrumbs) { @@ -169,7 +182,7 @@ export default class LeftPanel2 extends React.Component { ); } - let name = {displayName}; + let name = {OwnProfileStore.instance.displayName}; let buttons = ( @@ -186,8 +199,8 @@ export default class LeftPanel2 extends React.Component { { this.state = { menuDisplayed: false, - user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), isDarkTheme: this.isUserOnDarkTheme(), }; - } - private get displayName(): string { - if (MatrixClientPeg.get().isGuest()) { - return _t("Guest"); - } else if (this.state.user) { - return this.state.user.displayName; - } else { - return MatrixClientPeg.get().getUserId(); - } + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } private get hasHomePage(): boolean { @@ -81,6 +72,7 @@ export default class UserMenuButton extends React.Component { public componentWillUnmount() { if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } private isUserOnDarkTheme(): boolean { @@ -91,6 +83,12 @@ export default class UserMenuButton extends React.Component { return theme === "dark"; } + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + private onThemeChanged = () => { this.setState({isDarkTheme: this.isUserOnDarkTheme()}); }; @@ -209,7 +207,7 @@ export default class UserMenuButton extends React.Component {
- {this.displayName} + {OwnProfileStore.instance.displayName} {MatrixClientPeg.get().getUserId()} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 197fa109e8..74e747726a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -422,6 +422,7 @@ "Upgrade your Riot": "Upgrade your Riot", "A new version of Riot is available!": "A new version of Riot is available!", "You: %(message)s": "You: %(message)s", + "Guest": "Guest", "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", @@ -2059,7 +2060,6 @@ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", - "Guest": "Guest", "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts new file mode 100644 index 0000000000..45d8829e30 --- /dev/null +++ b/src/stores/OwnProfileStore.ts @@ -0,0 +1,122 @@ +/* +Copyright 2020 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. +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 { ActionPayload } from "../dispatcher/payloads"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { User } from "matrix-js-sdk/src/models/user"; +import { throttle } from "lodash"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { _t } from "../languageHandler"; + +interface IState { + displayName?: string; + avatarUrl?: string; +} + +export class OwnProfileStore extends AsyncStoreWithClient { + private static internalInstance = new OwnProfileStore(); + + private monitoredUser: User; + + private constructor() { + super(defaultDispatcher, {}); + } + + public static get instance(): OwnProfileStore { + return OwnProfileStore.internalInstance; + } + + /** + * Gets the display name for the user, or null if not present. + */ + public get displayName(): string { + if (!this.matrixClient) return this.state.displayName || null; + + if (this.matrixClient.isGuest()) { + return _t("Guest"); + } else if (this.state.displayName) { + return this.state.displayName; + } else { + return this.matrixClient.getUserId(); + } + } + + /** + * Gets the MXC URI of the user's avatar, or null if not present. + */ + public get avatarMxc(): string { + return this.state.avatarUrl || null; + } + + /** + * Gets the user's avatar as an HTTP URL of the given size. If the user's + * avatar is not present, this returns null. + * @param size The size of the avatar + * @returns The HTTP URL of the user's avatar + */ + public getHttpAvatarUrl(size: number): string { + if (!this.avatarMxc) return null; + return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size); + } + + protected async onNotReady() { + if (this.monitoredUser) { + this.monitoredUser.removeListener("User.displayName", this.onProfileUpdate); + this.monitoredUser.removeListener("User.avatarUrl", this.onProfileUpdate); + } + if (this.matrixClient) { + this.matrixClient.removeListener("RoomState.events", this.onStateEvents); + } + await this.reset({}); + } + + protected async onReady() { + const myUserId = this.matrixClient.getUserId(); + this.monitoredUser = this.matrixClient.getUser(myUserId); + if (this.monitoredUser) { + this.monitoredUser.on("User.displayName", this.onProfileUpdate); + this.monitoredUser.on("User.avatarUrl", this.onProfileUpdate); + } + + // We also have to listen for membership events for ourselves as the above User events + // are fired only with presence, which matrix.org (and many others) has disabled. + this.matrixClient.on("RoomState.events", this.onStateEvents); + + await this.onProfileUpdate(); // trigger an initial update + } + + protected async onAction(payload: ActionPayload) { + // we don't actually do anything here + } + + private onProfileUpdate = async () => { + // We specifically do not use the User object we stored for profile info as it + // could easily be wrong (such as per-room instead of global profile). + const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId()); + await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url}); + }; + + // TSLint wants this to be a member, but we don't want that. + // tslint:disable-next-line + private onStateEvents = throttle(async (ev: MatrixEvent) => { + const myUserId = MatrixClientPeg.get().getUserId(); + if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { + await this.onProfileUpdate(); + } + }, 200, {trailing: true, leading: true}); +} From 5c7e59b13200ab9dd652fca2f2e323d2dc982742 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 21:17:39 -0600 Subject: [PATCH 046/106] Use theme-capable icons in the user menu They're still inconsistent weights, but at least they are the right color on non-light-theme clients. --- res/css/structures/_UserMenuButton.scss | 66 +++++++++++++++++++- src/components/structures/UserMenuButton.tsx | 16 ++--- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 1fbbbb5fd8..768d643add 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -15,7 +15,26 @@ limitations under the License. */ .mx_UserMenuButton { - // No special styles on the button itself + > span { + width: 16px; + height: 16px; + position: relative; + display: block; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 0; + left: 0; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); + } + } } .mx_UserMenuButton_contextMenu { @@ -79,4 +98,49 @@ limitations under the License. justify-content: center; } } + + .mx_IconizedContextMenu_icon { + position: relative; + width: 16px; + height: 16px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_UserMenuButton_iconHome::before { + mask-image: url('$(res)/img/feather-customised/home.svg'); + } + + .mx_UserMenuButton_iconBell::before { + mask-image: url('$(res)/img/feather-customised/notifications.svg'); + } + + .mx_UserMenuButton_iconLock::before { + mask-image: url('$(res)/img/feather-customised/lock.svg'); + } + + .mx_UserMenuButton_iconSettings::before { + mask-image: url('$(res)/img/feather-customised/settings.svg'); + } + + .mx_UserMenuButton_iconArchive::before { + mask-image: url('$(res)/img/feather-customised/archive.svg'); + } + + .mx_UserMenuButton_iconMessage::before { + mask-image: url('$(res)/img/feather-customised/message-circle.svg'); + } + + .mx_UserMenuButton_iconSignOut::before { + mask-image: url('$(res)/img/feather-customised/sign-out.svg'); + } } diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 04b1b03368..f193a84648 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -190,7 +190,7 @@ export default class UserMenuButton extends React.Component { homeButton = (
  • - + {_t("Home")}
  • @@ -233,31 +233,31 @@ export default class UserMenuButton extends React.Component { {homeButton}
  • this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - + {_t("Notification settings")}
  • this.onSettingsOpen(e, USER_SECURITY_TAB)}> - + {_t("Security & privacy")}
  • this.onSettingsOpen(e, null)}> - + {_t("All settings")}
  • - + {_t("Archived rooms")}
  • - + {_t("Feedback")}
  • @@ -267,7 +267,7 @@ export default class UserMenuButton extends React.Component {
    • - + {_t("Sign out")}
    • @@ -287,7 +287,7 @@ export default class UserMenuButton extends React.Component { label={_t("Account settings")} isExpanded={this.state.menuDisplayed} > - ... + {/* masked image in CSS */} {contextMenu} From b3fd1eda03ebe6ac252d842b22b8a8db48a1463c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 24 Jun 2020 14:54:14 +0200 Subject: [PATCH 047/106] change the format of font faces to something closer to the css --- src/theme.js | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/theme.js b/src/theme.js index ee9cb9c7db..bfee597da4 100644 --- a/src/theme.js +++ b/src/theme.js @@ -49,15 +49,46 @@ function clearCustomTheme() { } } +const allowedFontFaceProps = [ + "font-display", + "font-family", + "font-stretch", + "font-style", + "font-weight", + "font-variant", + "font-feature-settings", + "font-variation-settings", + "src", + "unicode-range" +]; + function generateCustomFontFaceCSS(faces) { - return Object.entries(faces).map(([fontName, face]) => { - const src = Object.entries(face.src).map(([format, url]) => { - return `url('${url}') format('${format}')`; + return faces.map(face => { + const src = face.src && face.src.map(srcElement => { + let format; + if (srcElement.format) { + format = `format("${srcElement.format}")`; + } + if (srcElement.url) { + return `url("${srcElement.url}") ${format}`; + } else if (srcElement.local) { + return `local("${srcElement.local}") ${format}`; + } + return ""; }).join(", "); - return `@font-face {` + - ` font-family: '${fontName}';` + - ` src: ${src};` + - `}`; + const props = Object.keys(face).filter(prop => allowedFontFaceProps.includes(prop)); + const body = props.map(prop => { + let value; + if (prop === "src") { + value = src; + } else if (prop === "font-family") { + value = `"${face[prop]}"`; + } else { + value = face[prop]; + } + return `${prop}: ${value}`; + }).join(";"); + return `@font-face {${body}}`; }).join("\n"); } From 183eb78fa8867858ff5d005be81ff79d9ba127d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 24 Jun 2020 14:58:41 +0200 Subject: [PATCH 048/106] fix lint --- src/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.js b/src/theme.js index bfee597da4..c79e466933 100644 --- a/src/theme.js +++ b/src/theme.js @@ -59,7 +59,7 @@ const allowedFontFaceProps = [ "font-feature-settings", "font-variation-settings", "src", - "unicode-range" + "unicode-range", ]; function generateCustomFontFaceCSS(faces) { From 256636ccf843ba79d8d000cbc4f72ab57668600d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 07:04:19 -0600 Subject: [PATCH 049/106] Use display:block over absolute positioning --- res/css/structures/_UserMenuButton.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 768d643add..fe5b6f29a2 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -100,15 +100,14 @@ limitations under the License. } .mx_IconizedContextMenu_icon { - position: relative; width: 16px; height: 16px; + display: block; &::before { content: ''; width: 16px; height: 16px; - position: absolute; mask-position: center; mask-size: contain; mask-repeat: no-repeat; From b01015f5c5a45ca0031d3a1f2d26417d55ca4e20 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 24 Jun 2020 14:30:12 +0100 Subject: [PATCH 050/106] Show cross-signing / secret storage reset button in more cases This exposes the cross-signing / secret storage reset button in more cases to hopefully give people a better chance of trying again in case something failed halfway through set up. In particular, any combination of keys existing now reveals the reset button. Fixes https://github.com/vector-im/riot-web/issues/13993 --- .../views/settings/CrossSigningPanel.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 7eb239cbca..aa512d4365 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -154,13 +154,6 @@ export default class CrossSigningPanel extends React.PureComponent { errorSection =
      {error.toString()}
      ; } - // Whether the various keys exist on your account (but not necessarily - // on this device). - const enabledForAccount = ( - crossSigningPrivateKeysInStorage && - secretStorageKeyInAccount - ); - let summarisedStatus; if (homeserverSupportsCrossSigning === undefined) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); @@ -184,8 +177,19 @@ export default class CrossSigningPanel extends React.PureComponent { )}

      ; } + const keysExistAnywhere = ( + secretStorageKeyInAccount || + crossSigningPrivateKeysInStorage || + crossSigningPublicKeysOnDevice + ); + const keysExistEverywhere = ( + secretStorageKeyInAccount && + crossSigningPrivateKeysInStorage && + crossSigningPublicKeysOnDevice + ); + let resetButton; - if (enabledForAccount) { + if (keysExistAnywhere) { resetButton = (
      @@ -197,10 +201,7 @@ export default class CrossSigningPanel extends React.PureComponent { // TODO: determine how better to expose this to users in addition to prompts at login/toast let bootstrapButton; - if ( - (!enabledForAccount || !crossSigningPublicKeysOnDevice) && - homeserverSupportsCrossSigning - ) { + if (!keysExistEverywhere && homeserverSupportsCrossSigning) { bootstrapButton = (
      From 0638b94cc24333814542c4dae898ccfe504ba19a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 24 Jun 2020 16:06:50 +0100 Subject: [PATCH 051/106] Move compact checkbox --- .../tabs/user/_AppearanceUserSettingsTab.scss | 8 +++++++- src/components/views/elements/SettingsFlag.tsx | 14 ++++++++++++-- .../tabs/user/AppearanceUserSettingsTab.tsx | 10 ++++++++-- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index b24f548d60..d724b164e5 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -209,9 +209,15 @@ limitations under the License. } .mx_AppearanceUserSettingsTab_Advanced { + color: $primary-fg-color; + + > * { + margin-bottom: 16px; + } + .mx_AppearanceUserSettingsTab_AdvancedToggle { color: $accent-color; - margin-bottom: 16px; + cursor: pointer; } .mx_AppearanceUserSettingsTab_systemFont { diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 9bdd04d803..4f41db51e2 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -30,6 +30,7 @@ interface IProps { isExplicit?: boolean; // XXX: once design replaces all toggles make this the default useCheckbox?: boolean; + disabled?: boolean; onChange?(checked: boolean): void; } @@ -78,14 +79,23 @@ export default class SettingsFlag extends React.Component { else label = _t(label); if (this.props.useCheckbox) { - return + return {label} ; } else { return (
      {label} - +
      ); } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index e935663bbe..9846be18d3 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -391,7 +391,13 @@ export default class AppearanceUserSettingsTab extends React.Component + advanced = <> + -
      ; + ; } return
      {toggle} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 74e747726a..4970a650db 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -441,7 +441,7 @@ "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", - "Use compact timeline layout": "Use compact timeline layout", + "Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", "Show avatar changes": "Show avatar changes", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb882b2d18..6c26967b1e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -197,7 +197,7 @@ export const SETTINGS = { }, "useCompactLayout": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Use compact timeline layout'), + displayName: _td('Use a more compact ‘Modern’ layout'), default: false, }, "showRedactions": { From c047a76f1d60bb038f364278c5ea86335eca20a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 10:36:43 -0600 Subject: [PATCH 052/106] Update the filtering for the right tag --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 9eb0d27748..5f7a7bd2ef 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -217,7 +217,7 @@ export class Algorithm extends EventEmitter { // a room while filtering and it'll disappear. We don't update the filter earlier in // this function simply because we don't have to. this.recalculateFilteredRoomsForTag(tag); - if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(tag); + if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag); this.recalculateStickyRoom(); // Finally, trigger an update From 291914492fefae9162e84b904236baadb3afeb09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 19:20:43 -0600 Subject: [PATCH 053/106] Fix icons in the new user menu not showing up --- res/css/structures/_UserMenuButton.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 85c3f53aa1..c2bfe5b916 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -105,6 +105,7 @@ limitations under the License. content: ''; width: 16px; height: 16px; + display: block; mask-position: center; mask-size: contain; mask-repeat: no-repeat; From 752b2acc56007bd0ab70ac67eac509c029daece9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 12:23:09 -0600 Subject: [PATCH 054/106] Move MessagePreviewStore into the room list namespace --- src/components/views/rooms/RoomTile2.tsx | 2 +- src/stores/{ => room-list}/MessagePreviewStore.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename src/stores/{ => room-list}/MessagePreviewStore.ts (93%) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 18b4ee8185..63c9c1af23 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -34,7 +34,7 @@ import NotificationBadge, { import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; -import { MessagePreviewStore } from "../../../stores/MessagePreviewStore"; +import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomTileIcon from "./RoomTileIcon"; /******************************************************************* diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts similarity index 93% rename from src/stores/MessagePreviewStore.ts rename to src/stores/room-list/MessagePreviewStore.ts index 9e225d1709..29fa45d882 100644 --- a/src/stores/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -15,13 +15,13 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { ActionPayload } from "../dispatcher/payloads"; -import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; -import defaultDispatcher from "../dispatcher/dispatcher"; -import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; -import { textForEvent } from "../TextForEvent"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy"; +import { textForEvent } from "../../TextForEvent"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { _t } from "../languageHandler"; +import { _t } from "../../languageHandler"; const PREVIEWABLE_EVENTS = [ // This is the same list from RiotX From 37a415693f670e8193630c5913018c38e8d14e1c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 20:08:26 -0600 Subject: [PATCH 055/106] Allow the user to resize the new sublists to 1 tile For dogfooding https://github.com/vector-im/riot-web/issues/14137 To change the default: `localStorage.setItem("mx_dogfood_rl_defTiles", 4);` --- src/components/views/rooms/RoomSublist2.tsx | 2 +- src/stores/room-list/ListLayout.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 6e3e90f805..5cbe10e160 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -117,7 +117,7 @@ export default class RoomSublist2 extends React.Component { }; private onShowLessClick = () => { - this.props.layout.visibleTiles = this.props.layout.minVisibleTiles; + this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles; this.forceUpdate(); // because the layout doesn't trigger a re-render }; diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index ebc7b95854..370777ef8b 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -67,6 +67,7 @@ export class ListLayout { } public get visibleTiles(): number { + if (this._n === 0) return this.defaultVisibleTiles; return Math.max(this._n, this.minVisibleTiles); } @@ -78,7 +79,13 @@ export class ListLayout { public get minVisibleTiles(): number { // the .65 comes from the CSS where the show more button is // mathematically 65% of a tile when floating. - return 4.65; + return 1.65; + } + + public get defaultVisibleTiles(): number { + // TODO: Remove dogfood flag + const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4); + return val + 0.65; // see minVisibleTiles for where the .65 comes from } public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number { From 90ff4585d5979f354af2b118f52ca1e5f7400d53 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 20:14:01 -0600 Subject: [PATCH 056/106] Remove extraneous debug from the new left panel --- src/components/structures/LeftPanel2.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index ec846bd177..27583f26ee 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -161,7 +161,6 @@ export default class LeftPanel2 extends React.Component { }; private onResize = () => { - console.log("Resize width"); if (!this.listContainerRef.current) return; // ignore: no headers to sticky this.handleStickyHeaders(this.listContainerRef.current); }; From dc099efb19d41360dc248e626b65592a9b06baf0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 08:43:35 +0100 Subject: [PATCH 057/106] make Notifier getSoundForRoom synchronous Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Notifier.js | 4 ++-- .../settings/tabs/room/NotificationSettingsTab.js | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index cd328ba565..b6690959d2 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -122,7 +122,7 @@ const Notifier = { } }, - getSoundForRoom: async function(roomId) { + getSoundForRoom: function(roomId) { // We do no caching here because the SDK caches setting // and the browser will cache the sound. const content = SettingsStore.getValue("notificationSound", roomId); @@ -151,7 +151,7 @@ const Notifier = { }, _playAudioNotification: async function(ev, room) { - const sound = await this.getSoundForRoom(room.roomId); + const sound = this.getSoundForRoom(room.roomId); console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); try { diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index 96e6b3d354..c521e228e0 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -39,12 +39,11 @@ export default class NotificationsSettingsTab extends React.Component { // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs UNSAFE_componentWillMount() { // eslint-disable-line camelcase - Notifier.getSoundForRoom(this.props.roomId).then((soundData) => { - if (!soundData) { - return; - } - this.setState({currentSound: soundData.name || soundData.url}); - }); + const soundData = Notifier.getSoundForRoom(this.props.roomId); + if (!soundData) { + return; + } + this.setState({currentSound: soundData.name || soundData.url}); this._soundUpload = createRef(); } From dbe575d523b35cb3972e76e75e7ff681118df262 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 08:44:24 +0100 Subject: [PATCH 058/106] Remove DEBUG Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/toasts/AnalyticsToast.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx index 7cd59222dd..b186a65d9d 100644 --- a/src/toasts/AnalyticsToast.tsx +++ b/src/toasts/AnalyticsToast.tsx @@ -24,14 +24,12 @@ import GenericToast from "../components/views/toasts/GenericToast"; import ToastStore from "../stores/ToastStore"; const onAccept = () => { - console.log("DEBUG onAccept AnalyticsToast"); dis.dispatch({ action: 'accept_cookies', }); }; const onReject = () => { - console.log("DEBUG onReject AnalyticsToast"); dis.dispatch({ action: "reject_cookies", }); From d4eebd5202387dd4df9bff6710222827c995c614 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 08:58:54 +0100 Subject: [PATCH 059/106] Fix alwaysShowBadgeCounts settings defn Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb882b2d18..5948a4ec9e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -192,7 +192,7 @@ export const SETTINGS = { }, // TODO: Wire up appropriately to UI (FTUE notifications) "Notifications.alwaysShowBadgeCounts": { - supportedLevels: ['account'], + supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: false, }, "useCompactLayout": { From 8743af56ad8f9b71dc32f507fedede5b51b0d767 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 09:00:13 +0100 Subject: [PATCH 060/106] Bring notification utils into this century to simplify ongoing Notifications work Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../{ContentRules.js => ContentRules.ts} | 57 ++++++--- ...ificationUtils.js => NotificationUtils.ts} | 26 ++-- ...eVectorState.js => PushRuleVectorState.ts} | 47 ++++---- ...{StandardActions.js => StandardActions.ts} | 2 - src/notifications/types.ts | 111 ++++++++++++++++++ 5 files changed, 189 insertions(+), 54 deletions(-) rename src/notifications/{ContentRules.js => ContentRules.ts} (69%) rename src/notifications/{NotificationUtils.js => NotificationUtils.ts} (80%) rename src/notifications/{PushRuleVectorState.js => PushRuleVectorState.ts} (69%) rename src/notifications/{StandardActions.js => StandardActions.ts} (98%) create mode 100644 src/notifications/types.ts diff --git a/src/notifications/ContentRules.js b/src/notifications/ContentRules.ts similarity index 69% rename from src/notifications/ContentRules.js rename to src/notifications/ContentRules.ts index 8c285220c7..a3ec017e37 100644 --- a/src/notifications/ContentRules.js +++ b/src/notifications/ContentRules.ts @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -15,9 +15,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import {PushRuleVectorState, State} from "./PushRuleVectorState"; +import {IExtendedPushRule, IPushRuleSet, IRuleSets} from "./types"; -import {PushRuleVectorState} from "./PushRuleVectorState"; +export interface IContentRules { + vectorState: State; + rules: IExtendedPushRule[]; + externalRules: IExtendedPushRule[]; +} + +export const SCOPE = "global"; +export const KIND = "content"; export class ContentRules { /** @@ -31,7 +39,7 @@ export class ContentRules { * externalRules: a list of other keyword rules, with states other than * vectorState */ - static parseContentRules(rulesets) { + static parseContentRules(rulesets: IRuleSets): IContentRules { // first categorise the keyword rules in terms of their actions const contentRules = this._categoriseContentRules(rulesets); @@ -51,59 +59,72 @@ export class ContentRules { if (contentRules.loud.length) { return { - vectorState: PushRuleVectorState.LOUD, + vectorState: State.Loud, rules: contentRules.loud, - externalRules: [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other), + externalRules: [ + ...contentRules.loud_but_disabled, + ...contentRules.on, + ...contentRules.on_but_disabled, + ...contentRules.other, + ], }; } else if (contentRules.loud_but_disabled.length) { return { - vectorState: PushRuleVectorState.OFF, + vectorState: State.Off, rules: contentRules.loud_but_disabled, - externalRules: [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other), + externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on.length) { return { - vectorState: PushRuleVectorState.ON, + vectorState: State.On, rules: contentRules.on, - externalRules: [].concat(contentRules.on_but_disabled, contentRules.other), + externalRules: [...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on_but_disabled.length) { return { - vectorState: PushRuleVectorState.OFF, + vectorState: State.Off, rules: contentRules.on_but_disabled, externalRules: contentRules.other, }; } else { return { - vectorState: PushRuleVectorState.ON, + vectorState: State.On, rules: [], externalRules: contentRules.other, }; } } - static _categoriseContentRules(rulesets) { - const contentRules = {on: [], on_but_disabled: [], loud: [], loud_but_disabled: [], other: []}; + static _categoriseContentRules(rulesets: IRuleSets) { + const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = { + on: [], + on_but_disabled: [], + loud: [], + loud_but_disabled: [], + other: [], + }; + for (const kind in rulesets.global) { for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { const r = rulesets.global[kind][i]; // check it's not a default rule - if (r.rule_id[0] === '.' || kind !== 'content') { + if (r.rule_id[0] === '.' || kind !== "content") { continue; } - r.kind = kind; // is this needed? not sure + // this is needed as we are flattening an object of arrays into a single array + r.kind = kind; switch (PushRuleVectorState.contentRuleVectorStateKind(r)) { - case PushRuleVectorState.ON: + case State.On: if (r.enabled) { contentRules.on.push(r); } else { contentRules.on_but_disabled.push(r); } break; - case PushRuleVectorState.LOUD: + case State.Loud: if (r.enabled) { contentRules.loud.push(r); } else { diff --git a/src/notifications/NotificationUtils.js b/src/notifications/NotificationUtils.ts similarity index 80% rename from src/notifications/NotificationUtils.js rename to src/notifications/NotificationUtils.ts index bf393da060..e3b7f66447 100644 --- a/src/notifications/NotificationUtils.js +++ b/src/notifications/NotificationUtils.ts @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -15,7 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import {Action, Actions} from "./types"; + +interface IEncodedActions { + notify: boolean; + sound?: string; + highlight?: boolean; +} export class NotificationUtils { // Encodes a dictionary of { @@ -24,12 +30,12 @@ export class NotificationUtils { // "highlight: true/false, // } // to a list of push actions. - static encodeActions(action) { + static encodeActions(action: IEncodedActions) { const notify = action.notify; const sound = action.sound; const highlight = action.highlight; if (notify) { - const actions = ["notify"]; + const actions: Action[] = [Actions.Notify]; if (sound) { actions.push({"set_tweak": "sound", "value": sound}); } @@ -40,7 +46,7 @@ export class NotificationUtils { } return actions; } else { - return ["dont_notify"]; + return [Actions.DontNotify]; } } @@ -50,18 +56,18 @@ export class NotificationUtils { // "highlight: true/false, // } // If the actions couldn't be decoded then returns null. - static decodeActions(actions) { + static decodeActions(actions: Action[]): IEncodedActions { let notify = false; let sound = null; let highlight = false; for (let i = 0; i < actions.length; ++i) { const action = actions[i]; - if (action === "notify") { + if (action === Actions.Notify) { notify = true; - } else if (action === "dont_notify") { + } else if (action === Actions.DontNotify) { notify = false; - } else if (typeof action === 'object') { + } else if (typeof action === "object") { if (action.set_tweak === "sound") { sound = action.value; } else if (action.set_tweak === "highlight") { @@ -81,7 +87,7 @@ export class NotificationUtils { highlight = true; } - const result = {notify: notify, highlight: highlight}; + const result: IEncodedActions = { notify, highlight }; if (sound !== null) { result.sound = sound; } diff --git a/src/notifications/PushRuleVectorState.js b/src/notifications/PushRuleVectorState.ts similarity index 69% rename from src/notifications/PushRuleVectorState.js rename to src/notifications/PushRuleVectorState.ts index 263226ce1c..d33426cfc4 100644 --- a/src/notifications/PushRuleVectorState.js +++ b/src/notifications/PushRuleVectorState.ts @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -15,43 +15,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import {StandardActions} from "./StandardActions"; import {NotificationUtils} from "./NotificationUtils"; +import {IPushRule} from "./types"; + +export enum State { + /** The push rule is disabled */ + Off = "off", + /** The user will receive push notification for this rule */ + On = "on", + /** The user will receive push notification for this rule with sound and + highlight if this is legitimate */ + Loud = "loud", +} export class PushRuleVectorState { - // Backwards compatibility (things should probably be using .states instead) - static OFF = "off"; - static ON = "on"; - static LOUD = "loud"; + // Backwards compatibility (things should probably be using the enum above instead) + static OFF = State.Off; + static ON = State.On; + static LOUD = State.Loud; /** * Enum for state of a push rule as defined by the Vector UI. * @readonly * @enum {string} */ - static states = { - /** The push rule is disabled */ - OFF: PushRuleVectorState.OFF, - - /** The user will receive push notification for this rule */ - ON: PushRuleVectorState.ON, - - /** The user will receive push notification for this rule with sound and - highlight if this is legitimate */ - LOUD: PushRuleVectorState.LOUD, - }; + static states = State; /** * Convert a PushRuleVectorState to a list of actions * * @return [object] list of push-rule actions */ - static actionsFor(pushRuleVectorState) { - if (pushRuleVectorState === PushRuleVectorState.ON) { + static actionsFor(pushRuleVectorState: State) { + if (pushRuleVectorState === State.On) { return StandardActions.ACTION_NOTIFY; - } else if (pushRuleVectorState === PushRuleVectorState.LOUD) { + } else if (pushRuleVectorState === State.Loud) { return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND; } } @@ -63,7 +62,7 @@ export class PushRuleVectorState { * category or in PushRuleVectorState.LOUD, regardless of its enabled * state. Returns null if it does not match these categories. */ - static contentRuleVectorStateKind(rule) { + static contentRuleVectorStateKind(rule: IPushRule): State { const decoded = NotificationUtils.decodeActions(rule.actions); if (!decoded) { @@ -81,10 +80,10 @@ export class PushRuleVectorState { let stateKind = null; switch (tweaks) { case 0: - stateKind = PushRuleVectorState.ON; + stateKind = State.On; break; case 2: - stateKind = PushRuleVectorState.LOUD; + stateKind = State.Loud; break; } return stateKind; diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.ts similarity index 98% rename from src/notifications/StandardActions.js rename to src/notifications/StandardActions.ts index b54cea332a..c17010af9a 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.ts @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import {NotificationUtils} from "./NotificationUtils"; const encodeActions = NotificationUtils.encodeActions; diff --git a/src/notifications/types.ts b/src/notifications/types.ts new file mode 100644 index 0000000000..9622193740 --- /dev/null +++ b/src/notifications/types.ts @@ -0,0 +1,111 @@ +/* +Copyright 2020 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. +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. +*/ + +export enum NotificationSetting { + AllMessages = "all_messages", // .m.rule.message = notify + DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default. + MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread + Never = "never", // .m.rule.master = enabled (dont_notify) +} + +export interface ISoundTweak { + set_tweak: "sound"; + value: string; +} +export interface IHighlightTweak { + set_tweak: "highlight"; + value?: boolean; +} + +export type Tweak = ISoundTweak | IHighlightTweak; + +export enum Actions { + Notify = "notify", + DontNotify = "dont_notify", // no-op + Coalesce = "coalesce", // unused + MarkUnread = "mark_unread", // new +} + +export type Action = Actions | Tweak; + +// Push rule kinds in descending priority order +export enum Kind { + Override = "override", + ContentSpecific = "content", + RoomSpecific = "room", + SenderSpecific = "sender", + Underride = "underride", +} + +export interface IEventMatchCondition { + kind: "event_match"; + key: string; + pattern: string; +} + +export interface IContainsDisplayNameCondition { + kind: "contains_display_name"; +} + +export interface IRoomMemberCountCondition { + kind: "room_member_count"; + is: string; +} + +export interface ISenderNotificationPermissionCondition { + kind: "sender_notification_permission"; + key: string; +} + +export type Condition = + IEventMatchCondition | + IContainsDisplayNameCondition | + IRoomMemberCountCondition | + ISenderNotificationPermissionCondition; + +export enum RuleIds { + MasterRule = ".m.rule.master", // The master rule (all notifications disabling) + MessageRule = ".m.rule.message", + EncryptedMessageRule = ".m.rule.encrypted", + RoomOneToOneRule = ".m.rule.room_one_to_one", + EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one", +} + +export interface IPushRule { + enabled: boolean; + rule_id: RuleIds | string; + actions: Action[]; + default: boolean; + conditions?: Condition[]; // only applicable to `underride` and `override` rules + pattern?: string; // only applicable to `content` rules +} + +// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor +export interface IExtendedPushRule extends IPushRule { + kind: Kind; +} + +export interface IPushRuleSet { + override: IPushRule[]; + content: IPushRule[]; + room: IPushRule[]; + sender: IPushRule[]; + underride: IPushRule[]; +} + +export interface IRuleSets { + global: IPushRuleSet; +} From a3b38a2b5f0a683c9633a9c7fbb3b4b6264735d1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 07:14:02 -0600 Subject: [PATCH 061/106] Make LoggedInView a real component because it uses shouldComponentUpdate React demands this. --- src/components/structures/LoggedInView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 1bc656e6a3..9c01480df2 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -123,7 +123,7 @@ interface IState { * * Components mounted below us can access the matrix client via the react context. */ -class LoggedInView extends React.PureComponent { +class LoggedInView extends React.Component { static displayName = 'LoggedInView'; static propTypes = { From 41c59cc75e6bce6968e8e841306686663cd6edf9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 25 Jun 2020 14:21:08 +0100 Subject: [PATCH 062/106] Fix deactivated checked checkbox styling --- res/css/views/elements/_StyledCheckbox.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss index aab448605c..60f1bf0277 100644 --- a/res/css/views/elements/_StyledCheckbox.scss +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -77,8 +77,8 @@ limitations under the License. } &:checked:disabled + label > .mx_Checkbox_background { - background-color: $muted-fg-color; - border-color: rgba($muted-fg-color, 0.5); + background-color: $accent-color; + border-color: $accent-color; } } } From 61618d51628db53bc7175e1c820278930b2eb90c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 15:02:52 +0100 Subject: [PATCH 063/106] tidy up Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/RoomNotifs.js | 5 +++-- src/components/structures/MatrixChat.tsx | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index c67acaf314..4614bef378 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -56,10 +56,11 @@ export function countRoomsWithNotif(rooms) { } export function aggregateNotificationCount(rooms) { - return rooms.reduce((result, room, index) => { + return rooms.reduce((result, room) => { const roomNotifState = getRoomNotifsState(room.roomId); const highlight = room.getUnreadNotificationCount('highlight') > 0; - const notificationCount = room.getUnreadNotificationCount(); + // use helper method to include highlights in the previous version of the room + const notificationCount = getUnreadNotificationCount(room); const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index fa6cd8a4d8..63d2ec6fd9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1828,7 +1828,9 @@ export default class MatrixChat extends React.PureComponent { } updateStatusIndicator(state: string, prevState: string) { - const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count; + // only count visible rooms to not torment the user with notification counts in rooms they can't see + // it will include highlights from the previous version of the room internally + const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getVisibleRooms()).count; if (PlatformPeg.get()) { PlatformPeg.get().setErrorStatus(state === 'ERROR'); From ed634a2bde594bd67212b43b26a013115f1e0528 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 16:35:40 +0100 Subject: [PATCH 064/106] Add StyledRadioGroup to simplify use of StyledRadioButton and use in Appearance Tab Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/elements/StyledRadioGroup.tsx | 61 +++++++++++++++++++ .../tabs/user/AppearanceUserSettingsTab.tsx | 29 +++++---- 2 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/components/views/elements/StyledRadioGroup.tsx diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx new file mode 100644 index 0000000000..050a8b7adb --- /dev/null +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2020 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. +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 classNames from "classnames"; + +import StyledRadioButton from "./StyledRadioButton"; + +interface IDefinition { + value: T; + className?: string; + disabled?: boolean; + label: React.ReactChild; + description?: React.ReactChild; +} + +interface IProps { + name: string; + className?: string; + definitions: IDefinition[]; + value?: T; // if not provided no options will be selected + onChange(newValue: T); +} + +function StyledRadioGroup({name, definitions, value, className, onChange}: IProps) { + const _onChange = e => { + onChange(e.target.value); + }; + + return + {definitions.map(d => + + {d.label} + + {d.description} + )} + ; +} + +export default StyledRadioGroup; diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index e935663bbe..42f8cb01de 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -33,6 +33,7 @@ import StyledCheckbox from '../../../elements/StyledCheckbox'; import SettingsFlag from '../../../elements/SettingsFlag'; import Field from '../../../elements/Field'; import EventTilePreview from '../../../elements/EventTilePreview'; +import StyledRadioGroup from "../../../elements/StyledRadioGroup"; interface IProps { } @@ -116,8 +117,7 @@ export default class AppearanceUserSettingsTab extends React.Component): void => { - const newTheme = e.target.value; + private onThemeChange = (newTheme: string): void => { if (this.state.theme === newTheme) return; // doing getValue in the .catch will still return the value we failed to set, @@ -277,19 +277,18 @@ export default class AppearanceUserSettingsTab extends React.Component {_t("Theme")} {systemThemeSection} -
      - {orderedThemes.map(theme => { - return - {theme.name} - ; - })} +
      + ({ + value: t.id, + label: t.name, + disabled: this.state.useSystemTheme, + className: "mx_ThemeSelector_" + t.id, + }))} + onChange={this.onThemeChange} + value={this.state.useSystemTheme ? undefined : this.state.theme} + />
      {customThemeForm} From c920cf784e155f545264be990d5e2d1f3742efb5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 08:08:48 -0600 Subject: [PATCH 065/106] Create a StaticNotificationState for representative purposes --- src/components/views/rooms/NotificationBadge.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 523b5a55cc..6929341845 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -141,6 +141,20 @@ export default class NotificationBadge extends React.PureComponent Date: Thu, 25 Jun 2020 16:45:01 +0100 Subject: [PATCH 066/106] Add account and room-account data hooks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/hooks/useAccountData.ts | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/hooks/useAccountData.ts diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts new file mode 100644 index 0000000000..b9b89d1350 --- /dev/null +++ b/src/hooks/useAccountData.ts @@ -0,0 +1,50 @@ +/* +Copyright 2020 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. +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 {useCallback, useState} from "react"; +import {MatrixClient} from "matrix-js-sdk/src/client"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "./useEventEmitter"; + +const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined; + +// Hook to simplify listening to Matrix account data +export const useAccountData = (cli: MatrixClient, eventType: string) => { + const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); + + const handler = useCallback((event) => { + if (event.getType() !== eventType) return; + setValue(event.getContent()); + }, [cli, eventType]); + useEventEmitter(cli, "accountData", handler); + + return value || {}; +}; + +// Hook to simplify listening to Matrix room account data +export const useRoomAccountData = (room: Room, eventType: string) => { + const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType))); + + const handler = useCallback((event) => { + if (event.getType() !== eventType) return; + setValue(event.getContent()); + }, [room, eventType]); + useEventEmitter(room, "Room.accountData", handler); + + return value || {}; +}; From 4885615a40770ed7823e697e9bc79d9a4aacb790 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 16:55:38 +0100 Subject: [PATCH 067/106] improve typing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/hooks/useAccountData.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts index b9b89d1350..dd0d53f0d3 100644 --- a/src/hooks/useAccountData.ts +++ b/src/hooks/useAccountData.ts @@ -33,7 +33,7 @@ export const useAccountData = (cli: MatrixClient, eventType: strin }, [cli, eventType]); useEventEmitter(cli, "accountData", handler); - return value || {}; + return value || {} as T; }; // Hook to simplify listening to Matrix room account data @@ -46,5 +46,5 @@ export const useRoomAccountData = (room: Room, eventType: string) }, [room, eventType]); useEventEmitter(room, "Room.accountData", handler); - return value || {}; + return value || {} as T; }; From 5efa5d2c809582be2a51bdd2c6ea377d9a54ebb7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 10:07:23 -0600 Subject: [PATCH 068/106] Implement new resize handle for dogfooding Smaller handle width, small shadow on the top of the show more button if there's more rooms to be shown. The resize handle also only shows when you're hovering in the area now. The original design called for the shadow to show up only if the user is cutting a tile or dragging, however that is complicated implementation-wise. For speed and encouraging a dogfooding pattern we're going ahead with this behaviour instead. --- res/css/views/rooms/_RoomSublist2.scss | 40 +++++++++++++-------- src/components/views/rooms/RoomSublist2.tsx | 12 +++++-- src/stores/room-list/ListLayout.ts | 14 +++++--- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index c7dae56353..8dfc117533 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -226,6 +226,16 @@ limitations under the License. .mx_RoomSublist2_showLessButtonChevron { mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); } + + &.mx_RoomSublist2_isCutting::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + box-shadow: 0px -2px 3px rgba(46, 47, 50, 0.08); + } } // Class name comes from the ResizableBox component @@ -233,31 +243,31 @@ limitations under the License. // so that selector is below and one level higher. .react-resizable-handle { cursor: ns-resize; - border-radius: 2px; + border-radius: 3px; + + // Update the render() function for RoomSublist2 if this changes + height: 3px; // This is positioned directly below the 'show more' button. position: absolute; bottom: 0; - left: 0; - right: 0; - // This is to visually align the bar in the list. Should be 12px from - // either side of the list. We define this after the positioning to - // trick the browser. - margin-left: 4px; - margin-right: 4px; + // Together, these make the bar 48px wide + left: calc(50% - 24px); + right: calc(50% - 24px); + } + + // TODO: Use less sketchy selector by replacing the resize component entirely + // This causes flickering. + .mx_RoomSublist2_showNButton:hover + .react-resizable-handle, + .react-resizable-handle:hover { + opacity: 0.8; + background-color: $primary-fg-color; } } // The aforementioned selector for the hover state. &:hover, &.mx_RoomSublist2_hasMenuOpen { - .react-resizable-handle { - opacity: 0.2; - - // Update the render() function for RoomSublist2 if this changes - border: 2px solid $primary-fg-color; - } - &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer { // If the header doesn't have an aux button we still need to hide the badge for // the menu button. diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 5cbe10e160..015ad5b646 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -43,7 +43,7 @@ import { TagID } from "../../../stores/room-list/models"; *******************************************************************/ const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS -const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS +const RESIZE_HANDLE_HEIGHT = 3; // As defined by CSS const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT; @@ -356,6 +356,12 @@ export default class RoomSublist2 extends React.Component { const nVisible = Math.floor(layout.visibleTiles); const visibleTiles = tiles.slice(0, nVisible); + const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length); + const showMoreBtnClasses = classNames({ + 'mx_RoomSublist2_showNButton': true, + 'mx_RoomSublist2_isCutting': layout.visibleTiles < maxTilesFactored, + }); + // If we're hiding rooms, show a 'show more' button to the user. This button // floats above the resize handle, if we have one present. If the user has all // tiles visible, it becomes 'show less'. @@ -370,7 +376,7 @@ export default class RoomSublist2 extends React.Component { ); if (this.props.isMinimized) showMoreText = null; showNButton = ( -
      +
      {/* set by CSS masking */} @@ -386,7 +392,7 @@ export default class RoomSublist2 extends React.Component { ); if (this.props.isMinimized) showLessText = null; showNButton = ( -
      +
      {/* set by CSS masking */} diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 370777ef8b..8ca8ad637b 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -18,6 +18,10 @@ import { TagID } from "./models"; const TILE_HEIGHT_PX = 44; +// the .65 comes from the CSS where the show more button is +// mathematically 65% of a tile when floating. +const RESIZER_BOX_FACTOR = 0.65; + interface ISerializedListLayout { numTiles: number; showPreviews: boolean; @@ -77,15 +81,13 @@ export class ListLayout { } public get minVisibleTiles(): number { - // the .65 comes from the CSS where the show more button is - // mathematically 65% of a tile when floating. - return 1.65; + return 1 + RESIZER_BOX_FACTOR; } public get defaultVisibleTiles(): number { // TODO: Remove dogfood flag const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4); - return val + 0.65; // see minVisibleTiles for where the .65 comes from + return val + RESIZER_BOX_FACTOR; } public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number { @@ -99,6 +101,10 @@ export class ListLayout { return this.tilesToPixels(Math.min(maxTiles, n)) + padding; } + public tilesWithResizerBoxFactor(n: number): number { + return n + RESIZER_BOX_FACTOR; + } + public tilesWithPadding(n: number, paddingPx: number): number { return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx)); } From 0af1507eed14767b28e3acbbab8d505d96d02e18 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 10:54:51 -0600 Subject: [PATCH 069/106] Update sublists for new hover states Fixes https://github.com/vector-im/riot-web/issues/14135 Unblocks https://github.com/vector-im/riot-web/issues/14089 --- res/css/views/rooms/_RoomSublist2.scss | 110 ++++++-------------- src/components/views/rooms/RoomSublist2.tsx | 2 +- 2 files changed, 34 insertions(+), 78 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 8dfc117533..2efedc8cc9 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -85,23 +85,24 @@ limitations under the License. // *************************** .mx_RoomSublist2_badgeContainer { - opacity: 0.8; - width: 16px; - margin-right: 5px; // aligns with the room tile's badge - // Create another flexbox row because it's super easy to position the badge this way. display: flex; align-items: center; justify-content: center; + + // Apply the width and margin to the badge so the container doesn't occupy dead space + .mx_NotificationBadge { + width: 16px; + margin-left: 8px; // same as menu+aux buttons + } } - // Both of these buttons are hidden by default until the list is hovered .mx_RoomSublist2_auxButton, .mx_RoomSublist2_menuButton { - width: 0; - margin: 0; - visibility: hidden; + margin-left: 8px; // should be the same as the notification badge position: relative; + width: 24px; + height: 24px; border-radius: 32px; &::before { @@ -118,6 +119,13 @@ limitations under the License. } } + // Hide the menu button by default + .mx_RoomSublist2_menuButton { + visibility: hidden; + width: 0; + margin: 0; + } + .mx_RoomSublist2_auxButton::before { mask-image: url('$(res)/img/feather-customised/plus.svg'); } @@ -142,11 +150,9 @@ limitations under the License. .mx_RoomSublist2_collapseBtn { display: inline-block; position: relative; - - // Default hidden - visibility: hidden; - width: 0; - height: 0; + width: 12px; + height: 12px; + margin-right: 8px; &::before { content: ''; @@ -158,7 +164,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background-color: $primary-fg-color; mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } @@ -266,46 +272,12 @@ limitations under the License. } } - // The aforementioned selector for the hover state. - &:hover, &.mx_RoomSublist2_hasMenuOpen { - &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer { - // If the header doesn't have an aux button we still need to hide the badge for - // the menu button. - .mx_RoomSublist2_badgeContainer { - // Completely hide the badge - width: 0; - margin: 0; - visibility: hidden; - } - - &:not(.mx_RoomSublist2_headerContainer_withAux) { - // The menu button will be the rightmost button, so make it correctly aligned. - .mx_RoomSublist2_menuButton { - margin-right: 1px; // line it up with the badges on the room tiles - } - } - - // Both of these buttons have circled backgrounds and are visible at this point, - // so make them so. - .mx_RoomSublist2_auxButton, - .mx_RoomSublist2_menuButton { - width: 24px; - height: 24px; - margin-left: 16px; - visibility: visible; - background-color: $roomlist2-button-bg-color; - } - } - - .mx_RoomSublist2_headerContainer { - .mx_RoomSublist2_headerText { - .mx_RoomSublist2_collapseBtn { - visibility: visible; - width: 12px; - height: 12px; - margin-right: 4px; - } - } + &.mx_RoomSublist2_hasMenuOpen, + &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover { + .mx_RoomSublist2_menuButton { + visibility: visible; + width: 24px; + margin-left: 8px; } } @@ -354,7 +326,12 @@ limitations under the License. } } - &:hover, &.mx_RoomSublist2_hasMenuOpen { + .mx_RoomSublist2_menuButton { + height: 16px; + } + + &.mx_RoomSublist2_hasMenuOpen, + & > .mx_RoomSublist2_headerContainer:hover { .mx_RoomSublist2_menuButton { visibility: visible; position: absolute; @@ -375,7 +352,7 @@ limitations under the License. } } - .mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) { + &.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) { .mx_RoomSublist2_menuButton { bottom: 8px; // align to the middle of name, 40px less than the `bottom` above. } @@ -384,27 +361,6 @@ limitations under the License. } } -// We have a hover style on the room list with no specific list hovered, so account for that -.mx_RoomList2:hover .mx_RoomSublist2:not(.mx_RoomSublist2_minimized), -.mx_RoomSublist2_hasMenuOpen:not(.mx_RoomSublist2_minimized) { - .mx_RoomSublist2_headerContainer_withAux { - .mx_RoomSublist2_badgeContainer { - // Completely hide the badge - width: 0; - margin: 0; - visibility: hidden; - } - - .mx_RoomSublist2_auxButton { - // Show the aux button, but not the list button - width: 24px; - height: 24px; - margin-right: 1px; // line it up with the badges on the room tiles - visibility: visible; - } - } -} - .mx_RoomSublist2_contextMenu { padding: 20px 16px; width: 250px; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 015ad5b646..6510251bc5 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -320,8 +320,8 @@ export default class RoomSublist2 extends React.Component { {this.props.label} {this.renderMenu()} - {this.props.isMinimized ? null : addRoomButton} {this.props.isMinimized ? null : badgeContainer} + {this.props.isMinimized ? null : addRoomButton}
      {this.props.isMinimized ? badgeContainer : null} {this.props.isMinimized ? addRoomButton : null} From 3524d678f7f5dd1a842a9a409f3bee0b97b2bd64 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 21:24:24 +0100 Subject: [PATCH 070/106] Fix Welcome.html URLs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.ts | 15 ++++++++++----- src/components/views/auth/Welcome.js | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index d54dc7dd23..aed063ca32 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -227,6 +227,15 @@ export default abstract class BasePlatform { return url; } + // persist hs url and is url for when the user is returned to the app with the login token + // MUST be called before using URLs from getSSOCallbackUrl, internally called by startSingleSignOn + persistSSODetails(mxClient: MatrixClient) { + localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); + if (mxClient.getIdentityServerUrl()) { + localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); + } + } + /** * Begin Single Sign On flows. * @param {MatrixClient} mxClient the matrix client using which we should start the flow @@ -234,11 +243,7 @@ export default abstract class BasePlatform { * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. */ startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { - // persist hs url and is url for when the user is returned to the app with the login token - localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); - if (mxClient.getIdentityServerUrl()) { - localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); - } + this.persistSSODetails(mxClient); const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO } diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index 91ba368f70..c01b846739 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -45,8 +45,8 @@ export default class Welcome extends React.PureComponent { idBaseUrl: isUrl, }); const plaf = PlatformPeg.get(); - const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(), - this.props.fragmentAfterLogin); + plaf.persistSSODetails(tmpClient); + const callbackUrl = plaf.getSSOCallbackUrl(this.props.fragmentAfterLogin); return ( From 1c00ae8dd35857bc052b21ee882e863e205e9e2b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 21:59:46 +0100 Subject: [PATCH 071/106] Move to mx_sso_hs_url and co for sso persistance to not conflict with guest creds Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.ts | 21 ++++++++------------ src/Lifecycle.js | 9 ++++++--- src/components/structures/auth/SoftLogout.js | 6 +++--- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index aed063ca32..1d11495e61 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -25,8 +25,8 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload"; import {Action} from "./dispatcher/actions"; import {hideToast as hideUpdateToast} from "./toasts/UpdateToast"; -export const HOMESERVER_URL_KEY = "mx_hs_url"; -export const ID_SERVER_URL_KEY = "mx_is_url"; +export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; +export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; export enum UpdateCheckStatus { Checking = "CHECKING", @@ -221,21 +221,12 @@ export default abstract class BasePlatform { setLanguage(preferredLangs: string[]) {} - getSSOCallbackUrl(fragmentAfterLogin: string): URL { + protected getSSOCallbackUrl(fragmentAfterLogin: string): URL { const url = new URL(window.location.href); url.hash = fragmentAfterLogin || ""; return url; } - // persist hs url and is url for when the user is returned to the app with the login token - // MUST be called before using URLs from getSSOCallbackUrl, internally called by startSingleSignOn - persistSSODetails(mxClient: MatrixClient) { - localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); - if (mxClient.getIdentityServerUrl()) { - localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); - } - } - /** * Begin Single Sign On flows. * @param {MatrixClient} mxClient the matrix client using which we should start the flow @@ -243,7 +234,11 @@ export default abstract class BasePlatform { * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. */ startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { - this.persistSSODetails(mxClient); + // persist hs url and is url for when the user is returned to the app with the login token + localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); + if (mxClient.getIdentityServerUrl()) { + localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); + } const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 96cefaf593..facde3011c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -41,7 +41,10 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; -import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform"; +import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; + +export const HOMESERVER_URL_KEY = "mx_hs_url"; +export const ID_SERVER_URL_KEY = "mx_is_url"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -164,8 +167,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - const homeserver = localStorage.getItem(HOMESERVER_URL_KEY); - const identityServer = localStorage.getItem(ID_SERVER_URL_KEY); + const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY); + const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY); if (!homeserver) { console.warn("Cannot log in with token: can't determine HS URL to use"); return Promise.resolve(false); diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index a2824b63a3..6577386fae 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -25,7 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import SSOButton from "../../views/elements/SSOButton"; -import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform"; +import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; const LOGIN_VIEW = { LOADING: 1, @@ -158,8 +158,8 @@ export default class SoftLogout extends React.Component { async trySsoLogin() { this.setState({busy: true}); - const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); - const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl(); + const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl(); const loginType = "m.login.token"; const loginParams = { token: this.props.realQueryParams['loginToken'], From c65ccbcacf1ab3096c405434da6ec91349aebf75 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 22:00:22 +0100 Subject: [PATCH 072/106] Instead of passing sso and cas urls to Welcome, route via start_sso and start_cas Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 26 ++++++++++++++++++++---- src/components/views/auth/Welcome.js | 15 ++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d5f73fa3df..a48b1e62a9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -18,6 +18,7 @@ limitations under the License. */ import React, { createRef } from 'react'; +import { createClient } from "matrix-js-sdk"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -1612,6 +1613,19 @@ export default class MatrixChat extends React.PureComponent { }); } else if (screen === 'directory') { dis.fire(Action.ViewRoomDirectory); + } else if (screen === "start_sso" || screen === "start_cas") { + // TODO if logged in, skip SSO + let cli = MatrixClientPeg.get(); + if (!cli) { + const {hsUrl, isUrl} = this.props.serverConfig; + cli = createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + } + + const type = screen === "start_sso" ? "sso" : "cas"; + PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen === 'groups') { dis.dispatch({ action: 'view_my_groups', @@ -1922,9 +1936,7 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); }; - render() { - // console.log(`Rendering MatrixChat with view ${this.state.view}`); - + getFragmentAfterLogin() { let fragmentAfterLogin = ""; if (this.props.initialScreenAfterLogin && // XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop @@ -1932,7 +1944,13 @@ export default class MatrixChat extends React.PureComponent { ) { fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`; } + return fragmentAfterLogin; + } + render() { + // console.log(`Rendering MatrixChat with view ${this.state.view}`); + + const fragmentAfterLogin = this.getFragmentAfterLogin(); let view; if (this.state.view === Views.LOADING) { @@ -2011,7 +2029,7 @@ export default class MatrixChat extends React.PureComponent { } } else if (this.state.view === Views.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - view = ; + view = ; } else if (this.state.view === Views.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); view = ( diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index c01b846739..5a30a02490 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -18,9 +18,7 @@ import React from 'react'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; -import * as Matrix from "matrix-js-sdk"; import {_td} from "../../../languageHandler"; -import PlatformPeg from "../../../PlatformPeg"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -39,15 +37,6 @@ export default class Welcome extends React.PureComponent { pageUrl = 'welcome.html'; } - const {hsUrl, isUrl} = this.props.serverConfig; - const tmpClient = Matrix.createClient({ - baseUrl: hsUrl, - idBaseUrl: isUrl, - }); - const plaf = PlatformPeg.get(); - plaf.persistSSODetails(tmpClient); - const callbackUrl = plaf.getSSOCallbackUrl(this.props.fragmentAfterLogin); - return (
      @@ -55,8 +44,8 @@ export default class Welcome extends React.PureComponent { className="mx_WelcomePage" url={pageUrl} replaceMap={{ - "$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"), - "$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"), + "$riot:ssoUrl": "#/start_sso", + "$riot:casUrl": "#/start_cas", }} /> From f02c52b758439022b1746d0006d2e54dd1163ec0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 22:01:41 +0100 Subject: [PATCH 073/106] unexport things which need not exporting Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index facde3011c..9ae4ae7e03 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -43,8 +43,8 @@ import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; -export const HOMESERVER_URL_KEY = "mx_hs_url"; -export const ID_SERVER_URL_KEY = "mx_is_url"; +const HOMESERVER_URL_KEY = "mx_hs_url"; +const ID_SERVER_URL_KEY = "mx_is_url"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries From 29b0505bdbbfe21162d4a0c978a06238dc5f0991 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 22:02:39 +0100 Subject: [PATCH 074/106] Welcome no longer needs any props Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a48b1e62a9..26621fb439 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2029,7 +2029,7 @@ export default class MatrixChat extends React.PureComponent { } } else if (this.state.view === Views.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - view = ; + view = ; } else if (this.state.view === Views.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); view = ( From 2b58875c7f3859241ea811cf179d59b998cdeaa4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:07:38 -0600 Subject: [PATCH 075/106] Fix alignment issues with the user menu objects --- res/css/structures/_LeftPanel2.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index dd28a3107c..0765b628f6 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -48,7 +48,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations flex-direction: column; .mx_LeftPanel2_userHeader { - padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom + padding: 12px 12px 20px; // 12px top, 12px sides, 20px bottom // Create another flexbox column for the rows to stack within display: flex; @@ -65,6 +65,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_userAvatarContainer { position: relative; // to make default avatars work margin-right: 8px; + height: 32px; // to remove the unknown 4px gap the browser puts below it .mx_LeftPanel2_userAvatar { border-radius: 32px; // should match avatar size From 9f5a716cc5fd82a2af3d46aafab01e4293feecdd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:11:04 -0600 Subject: [PATCH 076/106] Adjust padding and margins on user menu --- res/css/_common.scss | 6 +++--- res/css/structures/_UserMenuButton.scss | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index e83c6aaeda..cf48358a4e 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -596,14 +596,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } &:last-child { - padding-bottom: 20px; + padding-bottom: 16px; } } .mx_IconizedContextMenu_optionList { // the notFirst class is for cases where the optionList might be under a header of sorts. &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { - margin-top: 20px; + margin-top: 12px; // This is a bit of a hack when we could just use a simple border-top property, // however we have a (kinda) good reason for doing it this way: we need opacity. @@ -634,7 +634,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { li { margin: 0; - padding: 20px 0 0; + padding: 12px 0 0; .mx_AccessibleButton { text-decoration: none; diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index c2bfe5b916..cdb6adb8bd 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -45,11 +45,6 @@ limitations under the License. display: flex; align-items: center; - &:nth-child(n + 1) { - // The first header will have appropriate padding, subsequent ones need a margin. - margin-top: 10px; - } - .mx_UserMenuButton_contextMenu_name { // Create another flexbox of columns to handle large user IDs display: flex; From 7b79dd6be14fcc3f7baa36e32e1bdab556f59706 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:13:28 -0600 Subject: [PATCH 077/106] Make the sign out button red --- res/css/structures/_UserMenuButton.scss | 10 ++++++++++ src/components/structures/UserMenuButton.tsx | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index cdb6adb8bd..f1dffbd1f5 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -40,6 +40,16 @@ limitations under the License. .mx_UserMenuButton_contextMenu { width: 247px; + .mx_UserMenuButton_contextMenu_redRow { + .mx_AccessibleButton { + color: $warning-color !important; // !important to override styles from context menu + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + .mx_UserMenuButton_contextMenu_header { // Create a flexbox to organize the header a bit easier display: flex; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 27dfdac5a1..7613a4a9ae 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -263,7 +263,7 @@ export default class UserMenuButton extends React.Component {
        -
      • +
      • {_t("Sign out")} From 129ff3a6e043799631ad0c6b2b02695c07abfb1f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:17:19 -0600 Subject: [PATCH 078/106] Match line colour from user menu in sublist menu --- res/css/views/rooms/_RoomSublist2.scss | 1 + res/themes/dark/css/_dark.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 8dfc117533..3117ebcc5f 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -414,6 +414,7 @@ limitations under the License. margin-bottom: 16px; margin-right: 16px; // additional 16px border: 1px solid $roomsublist2-divider-color; + opacity: 0.1; } .mx_RoomSublist2_contextMenu_title { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 69fc91f222..1546e7a400 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -113,7 +113,7 @@ $theme-button-bg-color: #e3e8f0; $roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist2-bg-color: $header-panel-bg-color; -$roomsublist2-divider-color: #e9eaeb; +$roomsublist2-divider-color: $primary-fg-color; $roomtile2-preview-color: #9e9e9e; $roomtile2-default-badge-bg-color: #61708b; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 57dc1fa5e0..c4b4262642 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -180,7 +180,7 @@ $theme-button-bg-color: #e3e8f0; $roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist2-bg-color: $header-panel-bg-color; -$roomsublist2-divider-color: #e9eaeb; +$roomsublist2-divider-color: $primary-fg-color; $roomtile2-preview-color: #9e9e9e; $roomtile2-default-badge-bg-color: #61708b; From aacedfaf1380d42d090136aeef737e8170ef42b9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:19:03 -0600 Subject: [PATCH 079/106] Remove opacity from sublist header text, increase weight --- res/css/views/rooms/_RoomSublist2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 3117ebcc5f..5f2e9e093b 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -130,9 +130,9 @@ limitations under the License. flex: 1; max-width: calc(100% - 16px); // 16px is the badge width text-transform: uppercase; - opacity: 0.5; line-height: $font-16px; font-size: $font-12px; + font-weight: 600; // Ellipsize any text overflow text-overflow: ellipsis; From 0cb54ed2a401646c8fab2d232f828131dcdac4ee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 15:42:44 -0600 Subject: [PATCH 080/106] Align the badge count on non-aux lists with other badges --- res/css/views/rooms/_RoomSublist2.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 2efedc8cc9..4f1c8e9323 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -97,6 +97,12 @@ limitations under the License. } } + &:not(.mx_RoomSublist2_headerContainer_withAux) { + .mx_NotificationBadge { + margin-right: 4px; // just to push it over a bit, aligning it with the other elements + } + } + .mx_RoomSublist2_auxButton, .mx_RoomSublist2_menuButton { margin-left: 8px; // should be the same as the notification badge From 555078a9935ade0ed3b0181a012a0a6a59cf2ef5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 16:03:56 -0600 Subject: [PATCH 081/106] Iterate on the new room list resize handle Only show shadow when resizing, increase the hit area, and make the handle show up when the list itself is hovered. --- res/css/views/rooms/_RoomSublist2.scss | 21 ++++++++++----------- src/components/views/rooms/RoomSublist2.tsx | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 5f2e9e093b..cbe471e4da 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -245,24 +245,23 @@ limitations under the License. cursor: ns-resize; border-radius: 3px; - // Update the render() function for RoomSublist2 if this changes - height: 3px; + // Update RESIZE_HANDLE_HEIGHT if this changes + height: 4px; // This is positioned directly below the 'show more' button. position: absolute; bottom: 0; - // Together, these make the bar 48px wide - left: calc(50% - 24px); - right: calc(50% - 24px); + // Together, these make the bar 64px wide + left: calc(50% - 32px); + right: calc(50% - 32px); } - // TODO: Use less sketchy selector by replacing the resize component entirely - // This causes flickering. - .mx_RoomSublist2_showNButton:hover + .react-resizable-handle, - .react-resizable-handle:hover { - opacity: 0.8; - background-color: $primary-fg-color; + &:hover, &.mx_RoomSublist2_hasMenuOpen { + .react-resizable-handle { + opacity: 0.8; + background-color: $primary-fg-color; + } } } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 015ad5b646..7273e80c61 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -43,7 +43,7 @@ import { TagID } from "../../../stores/room-list/models"; *******************************************************************/ const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS -const RESIZE_HANDLE_HEIGHT = 3; // As defined by CSS +const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT; @@ -70,6 +70,7 @@ interface IProps { interface IState { notificationState: ListNotificationState; menuDisplayed: boolean; + isResizing: boolean; } export default class RoomSublist2 extends React.Component { @@ -82,6 +83,7 @@ export default class RoomSublist2 extends React.Component { this.state = { notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId), menuDisplayed: false, + isResizing: false, }; this.state.notificationState.setRooms(this.props.rooms); } @@ -111,6 +113,14 @@ export default class RoomSublist2 extends React.Component { this.forceUpdate(); // because the layout doesn't trigger a re-render }; + private onResizeStart = () => { + this.setState({isResizing: true}); + }; + + private onResizeStop = () => { + this.setState({isResizing: false}); + }; + private onShowAllClick = () => { this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT); this.forceUpdate(); // because the layout doesn't trigger a re-render @@ -359,7 +369,7 @@ export default class RoomSublist2 extends React.Component { const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length); const showMoreBtnClasses = classNames({ 'mx_RoomSublist2_showNButton': true, - 'mx_RoomSublist2_isCutting': layout.visibleTiles < maxTilesFactored, + 'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored, }); // If we're hiding rooms, show a 'show more' button to the user. This button @@ -438,6 +448,8 @@ export default class RoomSublist2 extends React.Component { resizeHandles={handles} onResize={this.onResize} className="mx_RoomSublist2_resizeBox" + onResizeStart={this.onResizeStart} + onResizeStop={this.onResizeStop} > {visibleTiles} {showNButton} From acf56559e1a0349122cb464058752fc1e4f3f416 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 16:26:07 -0600 Subject: [PATCH 082/106] Introduce an entirely new system for handling message preview copy This reverts earlier changes made to textForEvent as they are no longer needed. This also implements an entire tree of textForEvent-like behaviour as the previews need to be different, which is easiest done with its own stack. --- src/TextForEvent.js | 23 +-- src/components/views/rooms/RoomTile2.tsx | 2 +- src/i18n/strings/en_EN.json | 56 +++++- src/stores/room-list/MessagePreviewStore.ts | 184 ++++++++++++------ .../previews/CallAnswerEventPreview.ts | 35 ++++ .../room-list/previews/CallHangupEvent.ts | 35 ++++ .../previews/CallInviteEventPreview.ts | 39 ++++ .../previews/CreationEventPreview.ts | 31 +++ .../previews/EncryptionEventPreview.ts | 31 +++ .../previews/HistoryVisibilityEventPreview.ts | 42 ++++ src/stores/room-list/previews/IPreview.ts | 31 +++ .../previews/MembershipEventPreview.ts | 90 +++++++++ .../room-list/previews/MessageEventPreview.ts | 55 ++++++ .../room-list/previews/NameEventPreview.ts | 31 +++ .../previews/ReactionEventPreview.ts | 34 ++++ .../room-list/previews/StickerEventPreview.ts | 34 ++++ .../previews/ThirdPartyInviteEventPreview.ts | 42 ++++ .../room-list/previews/TopicEventPreview.ts | 31 +++ src/stores/room-list/previews/utils.ts | 49 +++++ 19 files changed, 799 insertions(+), 76 deletions(-) create mode 100644 src/stores/room-list/previews/CallAnswerEventPreview.ts create mode 100644 src/stores/room-list/previews/CallHangupEvent.ts create mode 100644 src/stores/room-list/previews/CallInviteEventPreview.ts create mode 100644 src/stores/room-list/previews/CreationEventPreview.ts create mode 100644 src/stores/room-list/previews/EncryptionEventPreview.ts create mode 100644 src/stores/room-list/previews/HistoryVisibilityEventPreview.ts create mode 100644 src/stores/room-list/previews/IPreview.ts create mode 100644 src/stores/room-list/previews/MembershipEventPreview.ts create mode 100644 src/stores/room-list/previews/MessageEventPreview.ts create mode 100644 src/stores/room-list/previews/NameEventPreview.ts create mode 100644 src/stores/room-list/previews/ReactionEventPreview.ts create mode 100644 src/stores/room-list/previews/StickerEventPreview.ts create mode 100644 src/stores/room-list/previews/ThirdPartyInviteEventPreview.ts create mode 100644 src/stores/room-list/previews/TopicEventPreview.ts create mode 100644 src/stores/room-list/previews/utils.ts diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 09cfb67de7..3607d7a676 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -265,22 +265,13 @@ function textForServerACLEvent(ev) { return text + changes.join(" "); } -function textForMessageEvent(ev, skipUserPrefix) { +function textForMessageEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); let message = senderDisplayName + ': ' + ev.getContent().body; - if (skipUserPrefix) { - message = ev.getContent().body; - if (ev.getContent().msgtype === "m.emote") { - message = senderDisplayName + " " + message; - } else if (ev.getContent().msgtype === "m.image") { - message = _t('sent an image.'); - } - } else { - if (ev.getContent().msgtype === "m.emote") { - message = "* " + senderDisplayName + " " + message; - } else if (ev.getContent().msgtype === "m.image") { - message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); - } + if (ev.getContent().msgtype === "m.emote") { + message = "* " + senderDisplayName + " " + message; + } else if (ev.getContent().msgtype === "m.image") { + message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); } return message; } @@ -621,8 +612,8 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -export function textForEvent(ev, skipUserPrefix) { +export function textForEvent(ev) { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - if (handler) return handler(ev, skipUserPrefix); + if (handler) return handler(ev); return ''; } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 63c9c1af23..3d0a555877 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -271,7 +271,7 @@ export default class RoomTile2 extends React.Component { let messagePreview = null; if (this.props.showMessagePreview && !this.props.isMinimized) { // The preview store heavily caches this info, so should be safe to hammer. - const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room); + const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); // Only show the preview if there is one to show. if (text) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 74e747726a..86d5f488ff 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -247,7 +247,6 @@ "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.", - "sent an image.": "sent an image.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", @@ -421,12 +420,65 @@ "Restart": "Restart", "Upgrade your Riot": "Upgrade your Riot", "A new version of Riot is available!": "A new version of Riot is available!", - "You: %(message)s": "You: %(message)s", "Guest": "Guest", "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", + "You joined the call": "You joined the call", + "%(senderName)s joined the call": "%(senderName)s joined the call", + "Call in progress": "Call in progress", + "You left the call": "You left the call", + "%(senderName)s left the call": "%(senderName)s left the call", + "Call ended": "Call ended", + "You started a call": "You started a call", + "%(senderName)s started a call": "%(senderName)s started a call", + "Waiting for answer": "Waiting for answer", + "%(senderName)s is calling": "%(senderName)s is calling", + "You created the room": "You created the room", + "%(senderName)s created the room": "%(senderName)s created the room", + "You made the chat encrypted": "You made the chat encrypted", + "%(senderName)s made the chat encrypted": "%(senderName)s made the chat encrypted", + "You made history visible to new members": "You made history visible to new members", + "%(senderName)s made history visible to new members": "%(senderName)s made history visible to new members", + "You made history visible to anyone": "You made history visible to anyone", + "%(senderName)s made history visible to anyone": "%(senderName)s made history visible to anyone", + "You made history visible to future members": "You made history visible to future members", + "%(senderName)s made history visible to future members": "%(senderName)s made history visible to future members", + "You were invited": "You were invited", + "%(targetName)s was invited": "%(targetName)s was invited", + "You left": "You left", + "%(targetName)s left": "%(targetName)s left", + "You were kicked (%(reason)s)": "You were kicked (%(reason)s)", + "%(targetName)s was kicked (%(reason)s)": "%(targetName)s was kicked (%(reason)s)", + "You were kicked": "You were kicked", + "%(targetName)s was kicked": "%(targetName)s was kicked", + "You rejected the invite": "You rejected the invite", + "%(targetName)s rejected the invite": "%(targetName)s rejected the invite", + "You were uninvited": "You were uninvited", + "%(targetName)s was uninvited": "%(targetName)s was uninvited", + "You were banned (%(reason)s)": "You were banned (%(reason)s)", + "%(targetName)s was banned (%(reason)s)": "%(targetName)s was banned (%(reason)s)", + "You were banned": "You were banned", + "%(targetName)s was banned": "%(targetName)s was banned", + "You joined": "You joined", + "%(targetName)s joined": "%(targetName)s joined", + "You changed your name": "You changed your name", + "%(targetName)s changed their name": "%(targetName)s changed their name", + "You changed your avatar": "You changed your avatar", + "%(targetName)s changed their avatar": "%(targetName)s changed their avatar", + "%(senderName)s %(emote)s": "%(senderName)s %(emote)s", + "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", + "You changed the room name": "You changed the room name", + "%(senderName)s changed the room name": "%(senderName)s changed the room name", + "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", + "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", + "You uninvited %(targetName)s": "You uninvited %(targetName)s", + "%(senderName)s uninvited %(targetName)s": "%(senderName)s uninvited %(targetName)s", + "You invited %(targetName)s": "You invited %(targetName)s", + "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s", + "You changed the room topic": "You changed the room topic", + "%(senderName)s changed the room topic": "%(senderName)s changed the room topic", "Font scaling": "Font scaling", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 29fa45d882..b727069f9f 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -19,32 +19,86 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy"; -import { textForEvent } from "../../TextForEvent"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { _t } from "../../languageHandler"; +import { MessageEventPreview } from "./previews/MessageEventPreview"; +import { NameEventPreview } from "./previews/NameEventPreview"; +import { TagID } from "./models"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { TopicEventPreview } from "./previews/TopicEventPreview"; +import { MembershipEventPreview } from "./previews/MembershipEventPreview"; +import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview"; +import { CallInviteEventPreview } from "./previews/CallInviteEventPreview"; +import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview"; +import { CallHangupEvent } from "./previews/CallHangupEvent"; +import { EncryptionEventPreview } from "./previews/EncryptionEventPreview"; +import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview"; +import { StickerEventPreview } from "./previews/StickerEventPreview"; +import { ReactionEventPreview } from "./previews/ReactionEventPreview"; +import { CreationEventPreview } from "./previews/CreationEventPreview"; -const PREVIEWABLE_EVENTS = [ - // This is the same list from RiotX - {type: "m.room.message", isState: false}, - {type: "m.room.name", isState: true}, - {type: "m.room.topic", isState: true}, - {type: "m.room.member", isState: true}, - {type: "m.room.history_visibility", isState: true}, - {type: "m.call.invite", isState: false}, - {type: "m.call.hangup", isState: false}, - {type: "m.call.answer", isState: false}, - {type: "m.room.encrypted", isState: false}, - {type: "m.room.encryption", isState: true}, - {type: "m.room.third_party_invite", isState: true}, - {type: "m.sticker", isState: false}, - {type: "m.room.create", isState: true}, -]; +const PREVIEWS = { + 'm.room.message': { + isState: false, + previewer: new MessageEventPreview(), + }, + 'm.room.name': { + isState: true, + previewer: new NameEventPreview(), + }, + 'm.room.topic': { + isState: true, + previewer: new TopicEventPreview(), + }, + 'm.room.member': { + isState: true, + previewer: new MembershipEventPreview(), + }, + 'm.room.history_visibility': { + isState: true, + previewer: new HistoryVisibilityEventPreview(), + }, + 'm.call.invite': { + isState: false, + previewer: new CallInviteEventPreview(), + }, + 'm.call.answer': { + isState: false, + previewer: new CallAnswerEventPreview(), + }, + 'm.call.hangup': { + isState: false, + previewer: new CallHangupEvent(), + }, + 'm.room.encryption': { + isState: true, + previewer: new EncryptionEventPreview(), + }, + 'm.room.third_party_invite': { + isState: true, + previewer: new ThirdPartyInviteEventPreview(), + }, + 'm.sticker': { + isState: false, + previewer: new StickerEventPreview(), + }, + 'm.reaction': { + isState: false, + previewer: new ReactionEventPreview(), + }, + 'm.room.create': { + isState: true, + previewer: new CreationEventPreview(), + }, +}; // The maximum number of events we're willing to look back on to get a preview. const MAX_EVENTS_BACKWARDS = 50; +// type merging ftw +type TAG_ANY = "im.vector.any"; +const TAG_ANY: TAG_ANY = "im.vector.any"; + interface IState { - [roomId: string]: string | null; // null indicates the preview is empty + [roomId: string]: Map; // null indicates the preview is empty / irrelevant } export class MessagePreviewStore extends AsyncStoreWithClient { @@ -61,39 +115,76 @@ export class MessagePreviewStore extends AsyncStoreWithClient { /** * Gets the pre-translated preview for a given room * @param room The room to get the preview for. + * @param inTagId The tag ID in which the room resides * @returns The preview, or null if none present. */ - public getPreviewForRoom(room: Room): string { + public getPreviewForRoom(room: Room, inTagId: TagID): string { if (!room) return null; // invalid room, just return nothing - // It's faster to do a lookup this way than it is to use Object.keys().includes() - // We only want to generate a preview if there's one actually missing and not explicitly - // set as 'none'. const val = this.state[room.roomId]; - if (val !== null && typeof(val) !== "string") { - this.generatePreview(room); - } + if (!val) this.generatePreview(room, inTagId); - return this.state[room.roomId]; + const previews = this.state[room.roomId]; + if (!previews) return null; + + if (!previews.has(inTagId)) { + return previews.get(TAG_ANY); + } + return previews.get(inTagId); } - private generatePreview(room: Room) { + private generatePreview(room: Room, tagId?: TagID) { const events = room.timeline; if (!events) return; // should only happen in tests + let map = this.state[room.roomId]; + if (!map) { + map = new Map(); + + // We set the state later with the map, so no need to send an update now + } + + // Set the tags so we know what to generate + if (!map.has(TAG_ANY)) map.set(TAG_ANY, null); + if (tagId && !map.has(tagId)) map.set(tagId, null); + + let changed = false; for (let i = events.length - 1; i >= 0; i--) { if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached const event = events[i]; - const preview = this.generatePreviewForEvent(event); - if (preview.isPreviewable) { - // noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls - this.updateState({[room.roomId]: preview.preview}); - return; // break - we found some text + const previewDef = PREVIEWS[event.getType()]; + if (!previewDef) continue; + if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue; + + const anyPreview = previewDef.previewer.getTextFor(event, null); + if (!anyPreview) continue; // not previewable for some reason + + changed = changed || anyPreview !== map.get(TAG_ANY); + map.set(TAG_ANY, anyPreview); + + const tagsToGenerate = Array.from(map.keys()).filter(t => t !== TAG_ANY); // we did the any tag above + for (const genTagId of tagsToGenerate) { + const realTagId: TagID = genTagId === TAG_ANY ? null : genTagId; + const preview = previewDef.previewer.getTextFor(event, realTagId); + if (preview === anyPreview) { + changed = changed || anyPreview !== map.get(genTagId); + map.delete(genTagId); + } else { + changed = changed || preview !== map.get(genTagId); + map.set(genTagId, preview); + } } + + if (changed) { + // Update state for good measure - causes emit for update + // noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls + this.updateState({[room.roomId]: map}); + } + return; // we're done } - // if we didn't find anything, subscribe ourselves to an update + // At this point, we didn't generate a preview so clear it // noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls this.updateState({[room.roomId]: null}); } @@ -107,28 +198,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { const event = payload.event; // TODO: Type out the dispatcher if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important - - const preview = this.generatePreviewForEvent(event); - if (preview.isPreviewable) { - await this.updateState({[event.getRoomId()]: preview.preview}); - return; // break - we found some text - } + this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); } } - - private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } { - if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) { - const isSelf = event.getSender() === this.matrixClient.getUserId(); - let text = textForEvent(event, /*skipUserPrefix=*/isSelf); - if (!text || text.trim().length === 0) text = null; // force null if useless to us - if (text && isSelf) { - // XXX: i18n doesn't really work here if the language doesn't support prefixing. - // We'd ideally somehow route the `You:` bit to the textForEvent call, however - // threading that through is non-trivial. - text = _t("You: %(message)s", {message: text}); - } - return {isPreviewable: true, preview: text}; - } - return {isPreviewable: false, preview: null}; - } } diff --git a/src/stores/room-list/previews/CallAnswerEventPreview.ts b/src/stores/room-list/previews/CallAnswerEventPreview.ts new file mode 100644 index 0000000000..b7207307e2 --- /dev/null +++ b/src/stores/room-list/previews/CallAnswerEventPreview.ts @@ -0,0 +1,35 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class CallAnswerEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isSelf(event)) { + return _t("You joined the call"); + } else { + return _t("%(senderName)s joined the call", {senderName: getSenderName(event)}); + } + } else { + return _t("Call in progress"); + } + } +} diff --git a/src/stores/room-list/previews/CallHangupEvent.ts b/src/stores/room-list/previews/CallHangupEvent.ts new file mode 100644 index 0000000000..adc7d1aac8 --- /dev/null +++ b/src/stores/room-list/previews/CallHangupEvent.ts @@ -0,0 +1,35 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class CallHangupEvent implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isSelf(event)) { + return _t("You left the call"); + } else { + return _t("%(senderName)s left the call", {senderName: getSenderName(event)}); + } + } else { + return _t("Call ended"); + } + } +} diff --git a/src/stores/room-list/previews/CallInviteEventPreview.ts b/src/stores/room-list/previews/CallInviteEventPreview.ts new file mode 100644 index 0000000000..47486e3701 --- /dev/null +++ b/src/stores/room-list/previews/CallInviteEventPreview.ts @@ -0,0 +1,39 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class CallInviteEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isSelf(event)) { + return _t("You started a call"); + } else { + return _t("%(senderName)s started a call", {senderName: getSenderName(event)}); + } + } else { + if (isSelf(event)) { + return _t("Waiting for answer"); + } else { + return _t("%(senderName)s is calling", {senderName: getSenderName(event)}); + } + } + } +} diff --git a/src/stores/room-list/previews/CreationEventPreview.ts b/src/stores/room-list/previews/CreationEventPreview.ts new file mode 100644 index 0000000000..62bb5fe53a --- /dev/null +++ b/src/stores/room-list/previews/CreationEventPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class CreationEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (isSelf(event)) { + return _t("You created the room"); + } else { + return _t("%(senderName)s created the room", {senderName: getSenderName(event)}); + } + } +} diff --git a/src/stores/room-list/previews/EncryptionEventPreview.ts b/src/stores/room-list/previews/EncryptionEventPreview.ts new file mode 100644 index 0000000000..d00fd7e7f9 --- /dev/null +++ b/src/stores/room-list/previews/EncryptionEventPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class EncryptionEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (isSelf(event)) { + return _t("You made the chat encrypted"); + } else { + return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)}); + } + } +} diff --git a/src/stores/room-list/previews/HistoryVisibilityEventPreview.ts b/src/stores/room-list/previews/HistoryVisibilityEventPreview.ts new file mode 100644 index 0000000000..ac77a181f8 --- /dev/null +++ b/src/stores/room-list/previews/HistoryVisibilityEventPreview.ts @@ -0,0 +1,42 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class HistoryVisibilityEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + const visibility = event.getContent()['history_visibility']; + const isUs = isSelf(event); + + if (visibility === 'invited' || visibility === 'joined') { + return isUs + ? _t("You made history visible to new members") + : _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)}); + } else if (visibility === 'world_readable') { + return isUs + ? _t("You made history visible to anyone") + : _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)}); + } else { // shared, default + return isUs + ? _t("You made history visible to future members") + : _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)}); + } + } +} diff --git a/src/stores/room-list/previews/IPreview.ts b/src/stores/room-list/previews/IPreview.ts new file mode 100644 index 0000000000..9beb92bfbf --- /dev/null +++ b/src/stores/room-list/previews/IPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 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. +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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { TagID } from "../models"; + +/** + * Represents an event preview. + */ +export interface IPreview { + /** + * Gets the text which represents the event as a preview. + * @param event The event to preview. + * @param tagId Optional. The tag where the room the event was sent in resides. + * @returns The preview. + */ + getTextFor(event: MatrixEvent, tagId?: TagID): string; +} diff --git a/src/stores/room-list/previews/MembershipEventPreview.ts b/src/stores/room-list/previews/MembershipEventPreview.ts new file mode 100644 index 0000000000..44339aab5f --- /dev/null +++ b/src/stores/room-list/previews/MembershipEventPreview.ts @@ -0,0 +1,90 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getTargetName, isSelfTarget } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class MembershipEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + const newMembership = event.getContent()['membership']; + const oldMembership = event.getPrevContent()['membership']; + const reason = event.getContent()['reason']; + const isUs = isSelfTarget(event); + + if (newMembership === 'invite') { + return isUs + ? _t("You were invited") + : _t("%(targetName)s was invited", {targetName: getTargetName(event)}); + } else if (newMembership === 'leave' && oldMembership !== 'invite') { + if (event.getSender() === event.getStateKey()) { + return isUs + ? _t("You left") + : _t("%(targetName)s left", {targetName: getTargetName(event)}); + } else { + if (reason) { + return isUs + ? _t("You were kicked (%(reason)s)", {reason}) + : _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason}); + } else { + return isUs + ? _t("You were kicked") + : _t("%(targetName)s was kicked", {targetName: getTargetName(event)}); + } + } + } else if (newMembership === 'leave' && oldMembership === 'invite') { + if (event.getSender() === event.getStateKey()) { + return isUs + ? _t("You rejected the invite") + : _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)}); + } else { + return isUs + ? _t("You were uninvited") + : _t("%(targetName)s was uninvited", {targetName: getTargetName(event)}); + } + } else if (newMembership === 'ban') { + if (reason) { + return isUs + ? _t("You were banned (%(reason)s)", {reason}) + : _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason}); + } else { + return isUs + ? _t("You were banned") + : _t("%(targetName)s was banned", {targetName: getTargetName(event)}); + } + } else if (newMembership === 'join' && oldMembership !== 'join') { + return isUs + ? _t("You joined") + : _t("%(targetName)s joined", {targetName: getTargetName(event)}); + } else { + const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname']; + const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url']; + if (isDisplayNameChange) { + return isUs + ? _t("You changed your name") + : _t("%(targetName)s changed their name", {targetName: getTargetName(event)}); + } else if (isAvatarChange) { + return isUs + ? _t("You changed your avatar") + : _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)}); + } else { + return null; // no change + } + } + } +} diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts new file mode 100644 index 0000000000..6f0dc14a58 --- /dev/null +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -0,0 +1,55 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { _t } from "../../../languageHandler"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import ReplyThread from "../../../components/views/elements/ReplyThread"; + +export class MessageEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + let eventContent = event.getContent(); + + if (event.isRelation("m.replace")) { + // It's an edit, generate the preview on the new text + eventContent = event.getContent()['m.new_content']; + } + + let body = (eventContent['body'] || '').trim(); + const msgtype = eventContent['msgtype']; + if (!body || !msgtype) return null; // invalid event, no preview + + // XXX: Newer relations have a getRelation() function which is not compatible with replies. + const mRelatesTo = event.getWireContent()['m.relates_to']; + if (mRelatesTo && mRelatesTo['m.in_reply_to']) { + // If this is a reply, get the real reply and use that + body = (ReplyThread.stripPlainReply(body) || '').trim(); + if (!body) return null; // invalid event, no preview + } + + if (msgtype === 'm.emote') { + return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body}); + } + + if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + return body; + } else { + return _t("%(senderName)s: %(message)s", {senderName: getSenderName(event), message: body}); + } + } +} diff --git a/src/stores/room-list/previews/NameEventPreview.ts b/src/stores/room-list/previews/NameEventPreview.ts new file mode 100644 index 0000000000..4197abacfb --- /dev/null +++ b/src/stores/room-list/previews/NameEventPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class NameEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (isSelf(event)) { + return _t("You changed the room name"); + } else { + return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)}); + } + } +} diff --git a/src/stores/room-list/previews/ReactionEventPreview.ts b/src/stores/room-list/previews/ReactionEventPreview.ts new file mode 100644 index 0000000000..d58f592feb --- /dev/null +++ b/src/stores/room-list/previews/ReactionEventPreview.ts @@ -0,0 +1,34 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class ReactionEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + const reaction = event.getRelation().key; + if (!reaction) return; + + if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + return reaction; + } else { + return _t("%(senderName)s: %(reaction)s", {senderName: getSenderName(event), reaction}); + } + } +} diff --git a/src/stores/room-list/previews/StickerEventPreview.ts b/src/stores/room-list/previews/StickerEventPreview.ts new file mode 100644 index 0000000000..f8263a4a45 --- /dev/null +++ b/src/stores/room-list/previews/StickerEventPreview.ts @@ -0,0 +1,34 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class StickerEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + const stickerName = event.getContent()['body']; + if (!stickerName) return null; + + if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + return stickerName; + } else { + return _t("%(senderName)s: %(stickerName)s", {senderName: getSenderName(event), stickerName}); + } + } +} diff --git a/src/stores/room-list/previews/ThirdPartyInviteEventPreview.ts b/src/stores/room-list/previews/ThirdPartyInviteEventPreview.ts new file mode 100644 index 0000000000..b22cd9fac9 --- /dev/null +++ b/src/stores/room-list/previews/ThirdPartyInviteEventPreview.ts @@ -0,0 +1,42 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; +import { isValid3pidInvite } from "../../../RoomInvite"; + +export class ThirdPartyInviteEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (!isValid3pidInvite(event)) { + const targetName = event.getPrevContent().display_name || _t("Someone"); + if (isSelf(event)) { + return _t("You uninvited %(targetName)s", {targetName}); + } else { + return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName}); + } + } else { + const targetName = event.getContent().display_name; + if (isSelf(event)) { + return _t("You invited %(targetName)s", {targetName}); + } else { + return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName}); + } + } + } +} diff --git a/src/stores/room-list/previews/TopicEventPreview.ts b/src/stores/room-list/previews/TopicEventPreview.ts new file mode 100644 index 0000000000..9b499aae8f --- /dev/null +++ b/src/stores/room-list/previews/TopicEventPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 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. +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 { IPreview } from "./IPreview"; +import { TagID } from "../models"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { getSenderName, isSelf } from "./utils"; +import { _t } from "../../../languageHandler"; + +export class TopicEventPreview implements IPreview { + public getTextFor(event: MatrixEvent, tagId?: TagID): string { + if (isSelf(event)) { + return _t("You changed the room topic"); + } else { + return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)}); + } + } +} diff --git a/src/stores/room-list/previews/utils.ts b/src/stores/room-list/previews/utils.ts new file mode 100644 index 0000000000..ebbecd7bbd --- /dev/null +++ b/src/stores/room-list/previews/utils.ts @@ -0,0 +1,49 @@ +/* +Copyright 2020 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. +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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { DefaultTagID, TagID } from "../models"; + +export function isSelf(event: MatrixEvent): boolean { + const selfUserId = MatrixClientPeg.get().getUserId(); + if (event.getType() === 'm.room.member') { + return event.getStateKey() === selfUserId; + } + return event.getSender() === selfUserId; +} + +export function isSelfTarget(event: MatrixEvent): boolean { + const selfUserId = MatrixClientPeg.get().getUserId(); + return event.getStateKey() === selfUserId; +} + +export function shouldPrefixMessagesIn(roomId: string, tagId: TagID): boolean { + if (tagId !== DefaultTagID.DM) return true; + + // We don't prefix anything in 1:1s + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return true; + return room.currentState.getJoinedMemberCount() !== 2; +} + +export function getSenderName(event: MatrixEvent): string { + return event.sender ? event.sender.name : event.getSender(); +} + +export function getTargetName(event: MatrixEvent): string { + return event.target ? event.target.name : event.getStateKey(); +} From 6116cfc2b96d753dbbe32b88c0ae8f715ad9a2de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Jun 2020 23:52:32 +0100 Subject: [PATCH 083/106] js-sdk imports suck Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 26621fb439..7f838f1e9e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import React, { createRef } from 'react'; -import { createClient } from "matrix-js-sdk"; +import { createClient } from "matrix-js-sdk/src"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -1948,8 +1948,6 @@ export default class MatrixChat extends React.PureComponent { } render() { - // console.log(`Rendering MatrixChat with view ${this.state.view}`); - const fragmentAfterLogin = this.getFragmentAfterLogin(); let view; From 6ea5dc7b7c550a9edd2c79735c8bba8883841a4e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 24 Apr 2020 22:14:41 +0100 Subject: [PATCH 084/106] Change the look of the spinner --- res/css/_common.scss | 4 ++ res/css/views/elements/_InlineSpinner.scss | 8 ++++ res/css/views/elements/_Spinner.scss | 10 +++++ res/img/spinner.svg | 3 ++ src/components/structures/MessagePanel.js | 2 +- src/components/views/elements/AppTile.js | 4 +- .../views/elements/InlineSpinner.js | 10 ++++- .../views/elements/MessageSpinner.js | 35 ----------------- src/components/views/elements/Spinner.js | 38 ++++++++++++------- src/components/views/messages/MAudioBody.js | 3 +- src/components/views/messages/MImageBody.js | 8 +--- src/components/views/messages/MVideoBody.js | 3 +- 12 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 res/img/spinner.svg delete mode 100644 src/components/views/elements/MessageSpinner.js diff --git a/res/css/_common.scss b/res/css/_common.scss index cf48358a4e..c087df04cb 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -428,6 +428,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border-radius: 8px; padding: 0px; box-shadow: none; + + /* Don't show scroll-bars on spinner dialogs */ + overflow-x: hidden; + overflow-y: hidden; } // TODO: Review mx_GeneralButton usage to see if it can use a different class diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 612b6209c6..561b6cfb82 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -21,4 +21,12 @@ limitations under the License. .mx_InlineSpinner img { margin: 0px 6px; vertical-align: -3px; + + animation: spin 1s linear infinite; +} + +@keyframes spin { + 100% { + transform: rotate(360deg); + } } diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index 01b4f23c2c..db9f54b56c 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -23,6 +23,16 @@ limitations under the License. flex: 1; } +.mx_Spinner img { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} + .mx_MatrixChat_middlePanel .mx_Spinner { height: auto; } diff --git a/res/img/spinner.svg b/res/img/spinner.svg new file mode 100644 index 0000000000..a18140c7e2 --- /dev/null +++ b/res/img/spinner.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d11fee6360..481741dfd2 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -770,7 +770,7 @@ export default class MessagePanel extends React.Component { topSpinner =
      • ; } if (this.props.forwardPaginating) { - bottomSpinner =
      • ; + bottomSpinner =
      • ; } const style = this.props.hidden ? { display: 'none' } : {}; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9129b8fe48..60cd1a2eba 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import AppPermission from './AppPermission'; import AppWarning from './AppWarning'; -import MessageSpinner from './MessageSpinner'; +import Spinner from './Spinner'; import WidgetUtils from '../../../utils/WidgetUtils'; import dis from '../../../dispatcher/dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; @@ -740,7 +740,7 @@ export default class AppTile extends React.Component { if (this.props.show) { const loadingElement = (
        - +
        ); if (!this.state.hasPermissionToLoad) { diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index ad70471d89..4842cba0c6 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; +import {_t} from "../../../languageHandler"; export default createReactClass({ displayName: 'InlineSpinner', @@ -24,10 +25,17 @@ export default createReactClass({ const w = this.props.w || 16; const h = this.props.h || 16; const imgClass = this.props.imgClassName || ""; + const alt = this.props.alt || _t("Loading..."); return (
        - + {alt}
        ); }, diff --git a/src/components/views/elements/MessageSpinner.js b/src/components/views/elements/MessageSpinner.js deleted file mode 100644 index 1775fdd4d7..0000000000 --- a/src/components/views/elements/MessageSpinner.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2017 Vector Creations 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 createReactClass from 'create-react-class'; - -export default createReactClass({ - displayName: 'MessageSpinner', - - render: function() { - const w = this.props.w || 32; - const h = this.props.h || 32; - const imgClass = this.props.imgClassName || ""; - const msg = this.props.msg || "Loading..."; - return ( -
        -
        { msg }
          - -
        - ); - }, -}); diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index b1fe97d5d2..3d5697c72e 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -16,19 +16,29 @@ limitations under the License. */ import React from "react"; -import createReactClass from 'create-react-class'; +import PropTypes from "prop-types"; +import {_t} from "../../../languageHandler"; -export default createReactClass({ - displayName: 'Spinner', +const Spinner = ({w = 32, h = 32, imgClassName, alt, message}) => { + return ( +
        + { message &&
        { message}
         
        } + {alt +
        + ); +}; +Spinner.propTypes = { + w: PropTypes.number, + h: PropTypes.number, + imgClassName: PropTypes.string, + alt: PropTypes.string, + message: PropTypes.node, +}; - render: function() { - const w = this.props.w || 32; - const h = this.props.h || 32; - const imgClass = this.props.imgClassName || ""; - return ( -
        - -
        - ); - }, -}); +export default Spinner; diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index a642936fec..421ec8fc47 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -22,6 +22,7 @@ import MFileBody from './MFileBody'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; +import InlineSpinner from '../elements/InlineSpinner'; export default class MAudioBody extends React.Component { constructor(props) { @@ -94,7 +95,7 @@ export default class MAudioBody extends React.Component { // Not sure how tall the audio player is so not sure how tall it should actually be. return ( - {content.body} + ); } diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index ad238a728e..4d12dcf5d7 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -26,6 +26,7 @@ import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import InlineSpinner from '../elements/InlineSpinner'; export default class MImageBody extends React.Component { static propTypes = { @@ -365,12 +366,7 @@ export default class MImageBody extends React.Component { // e2e image hasn't been decrypted yet if (content.file !== undefined && this.state.decryptedUrl === null) { - placeholder = {content.body}; + placeholder = ; } else if (!this.state.imgLoaded) { // Deliberately, getSpinner is left unimplemented here, MStickerBody overides placeholder = this.getPlaceholder(); diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 03f345e042..1fba9d2ead 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; +import InlineSpinner from '../elements/InlineSpinner'; export default createReactClass({ displayName: 'MVideoBody', @@ -147,7 +148,7 @@ export default createReactClass({ return (
        - {content.body} +
        ); From 87f961df3f99feb0cbcc784a65550bbdbf42c4c5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 00:00:30 +0100 Subject: [PATCH 085/106] Put behind a labs flag --- res/css/views/elements/_Spinner.scss | 2 +- .../views/elements/InlineSpinner.js | 15 +++++++++-- src/components/views/elements/Spinner.js | 27 +++++++++++++------ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 +++++ 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index db9f54b56c..6966a60e52 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -23,7 +23,7 @@ limitations under the License. flex: 1; } -.mx_Spinner img { +.mx_Spinner_spin img { animation: spin 1s linear infinite; } diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index 4842cba0c6..dba2479d57 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -17,6 +17,7 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; import {_t} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; export default createReactClass({ displayName: 'InlineSpinner', @@ -27,10 +28,20 @@ export default createReactClass({ const imgClass = this.props.imgClassName || ""; const alt = this.props.alt || _t("Loading..."); + let divClass; + let imageSource; + if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + divClass = "mx_InlineSpinner mx_Spinner_spin"; + imageSource = require("../../../../res/img/spinner.svg"); + } else { + divClass = "mx_InlineSpinner"; + imageSource = require("../../../../res/img/spinner.gif"); + } + return ( -
        +
        { + let divClass; + let imageSource; + if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + divClass = "mx_Spinner mx_Spinner_spin"; + imageSource = require("../../../../res/img/spinner.svg"); + } else { + divClass = "mx_Spinner"; + imageSource = require("../../../../res/img/spinner.gif"); + } + return ( -
        +
        { message &&
        { message}
         
        } - {alt + {alt
        ); }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4970a650db..ae44d59c59 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -427,6 +427,7 @@ "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", + "New spinner design": "New spinner design", "Font scaling": "Font scaling", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f19b827307..820329f6c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -97,6 +97,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "feature_new_spinner": { + isFeature: true, + displayName: _td("New spinner design"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_font_scaling": { isFeature: true, displayName: _td("Font scaling"), From b00d822bc0782c07622068af8edc2672fce934e0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 01:18:02 +0100 Subject: [PATCH 086/106] Remove alt, use aria-label --- src/components/views/elements/InlineSpinner.js | 3 +-- src/components/views/elements/Spinner.js | 5 ++--- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index dba2479d57..ad88868790 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -26,7 +26,6 @@ export default createReactClass({ const w = this.props.w || 16; const h = this.props.h || 16; const imgClass = this.props.imgClassName || ""; - const alt = this.props.alt || _t("Loading..."); let divClass; let imageSource; @@ -45,7 +44,7 @@ export default createReactClass({ width={w} height={h} className={imgClass} - alt={alt} + aria-label={_t("Loading...")} />
        ); diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index c192f7d499..ee4351fed6 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -20,7 +20,7 @@ import PropTypes from "prop-types"; import {_t} from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; -const Spinner = ({w = 32, h = 32, imgClassName, alt, message}) => { +const Spinner = ({w = 32, h = 32, imgClassName, message}) => { let divClass; let imageSource; if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { @@ -39,7 +39,7 @@ const Spinner = ({w = 32, h = 32, imgClassName, alt, message}) => { width={w} height={h} className={imgClassName} - alt={alt || _t("Loading...")} + aria-label={_t("Loading...")} />
        ); @@ -48,7 +48,6 @@ Spinner.propTypes = { w: PropTypes.number, h: PropTypes.number, imgClassName: PropTypes.string, - alt: PropTypes.string, message: PropTypes.node, }; diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 421ec8fc47..37f85a108f 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -95,7 +95,7 @@ export default class MAudioBody extends React.Component { // Not sure how tall the audio player is so not sure how tall it should actually be. return ( - + ); } diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 4d12dcf5d7..c92ae475bf 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -366,7 +366,7 @@ export default class MImageBody extends React.Component { // e2e image hasn't been decrypted yet if (content.file !== undefined && this.state.decryptedUrl === null) { - placeholder = ; + placeholder = ; } else if (!this.state.imgLoaded) { // Deliberately, getSpinner is left unimplemented here, MStickerBody overides placeholder = this.getPlaceholder(); diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 1fba9d2ead..fdc04deffc 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -148,7 +148,7 @@ export default createReactClass({ return (
        - +
        ); From dafce40d1b7f1a5b6bf8f3f69a1034810fa3953d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 19:29:12 -0600 Subject: [PATCH 087/106] Rename UserMenuButton to UserMenu for new scope --- res/css/_components.scss | 2 +- res/css/structures/{_UserMenuButton.scss => _UserMenu.scss} | 0 src/components/structures/LeftPanel2.tsx | 4 ++-- .../structures/{UserMenuButton.tsx => UserMenu.tsx} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename res/css/structures/{_UserMenuButton.scss => _UserMenu.scss} (100%) rename src/components/structures/{UserMenuButton.tsx => UserMenu.tsx} (99%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 66eb98ea9d..afc40ca0d6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -30,7 +30,7 @@ @import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; -@import "./structures/_UserMenuButton.scss"; +@import "./structures/_UserMenu.scss"; @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenu.scss similarity index 100% rename from res/css/structures/_UserMenuButton.scss rename to res/css/structures/_UserMenu.scss diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 27583f26ee..4731dad1fc 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -24,7 +24,7 @@ import RoomList2 from "../views/rooms/RoomList2"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseAvatar from '../views/avatars/BaseAvatar'; -import UserMenuButton from "./UserMenuButton"; +import UserMenu from "./UserMenuButton"; import RoomSearch from "./RoomSearch"; import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; @@ -184,7 +184,7 @@ export default class LeftPanel2 extends React.Component { let name = {OwnProfileStore.instance.displayName}; let buttons = ( - + ); if (this.props.isMinimized) { diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenu.tsx similarity index 99% rename from src/components/structures/UserMenuButton.tsx rename to src/components/structures/UserMenu.tsx index 7613a4a9ae..c927e5c7a7 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenu.tsx @@ -44,7 +44,7 @@ interface IState { isDarkTheme: boolean; } -export default class UserMenuButton extends React.Component { +export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; private buttonRef: React.RefObject = createRef(); From bcfdd4d98406ed26c3df7b3e10bd0de92ed2a6dd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 19:38:11 -0600 Subject: [PATCH 088/106] Move all of the UserMenu into the UserMenu component --- res/css/structures/_LeftPanel2.scss | 28 +--------- res/css/structures/_UserMenu.scss | 60 ++++++++++++++++----- src/components/structures/LeftPanel2.tsx | 54 +------------------ src/components/structures/UserMenu.tsx | 67 ++++++++++++++++++------ 4 files changed, 100 insertions(+), 109 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 0765b628f6..837a1d1f0d 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -54,7 +54,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations display: flex; flex-direction: column; - // There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs + // This is basically just breadcrumbs. The row above that is handled by the UserMenu .mx_LeftPanel2_headerRow { // Create yet another flexbox, this time within the row, to ensure items stay // aligned correctly. This is also a row-based flexbox. @@ -62,32 +62,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations align-items: center; } - .mx_LeftPanel2_userAvatarContainer { - position: relative; // to make default avatars work - margin-right: 8px; - height: 32px; // to remove the unknown 4px gap the browser puts below it - - .mx_LeftPanel2_userAvatar { - border-radius: 32px; // should match avatar size - } - } - - .mx_LeftPanel2_userName { - font-weight: 600; - font-size: $font-15px; - line-height: $font-20px; - flex: 1; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .mx_LeftPanel2_headerButtons { - // No special styles: the rest of the layout happens to make it work. - } - .mx_LeftPanel2_breadcrumbsContainer { width: 100%; overflow: hidden; diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index f1dffbd1f5..a15eeb6c81 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -14,6 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_UserMenu { + // Create a row-based flexbox to ensure items stay aligned correctly. + display: flex; + align-items: center; + + .mx_UserMenu_userAvatarContainer { + position: relative; // to make default avatars work + margin-right: 8px; + height: 32px; // to remove the unknown 4px gap the browser puts below it + + .mx_UserMenu_userAvatar { + border-radius: 32px; // should match avatar size + } + } + + .mx_UserMenu_userName { + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenu_headerButtons { + // No special styles: the rest of the layout happens to make it work. + } +} + .mx_UserMenuButton { > span { width: 16px; @@ -37,10 +69,10 @@ limitations under the License. } } -.mx_UserMenuButton_contextMenu { +.mx_UserMenu_contextMenu { width: 247px; - .mx_UserMenuButton_contextMenu_redRow { + .mx_UserMenu_contextMenu_redRow { .mx_AccessibleButton { color: $warning-color !important; // !important to override styles from context menu } @@ -50,12 +82,12 @@ limitations under the License. } } - .mx_UserMenuButton_contextMenu_header { + .mx_UserMenu_contextMenu_header { // Create a flexbox to organize the header a bit easier display: flex; align-items: center; - .mx_UserMenuButton_contextMenu_name { + .mx_UserMenu_contextMenu_name { // Create another flexbox of columns to handle large user IDs display: flex; flex-direction: column; @@ -72,19 +104,19 @@ limitations under the License. white-space: nowrap; } - .mx_UserMenuButton_contextMenu_displayName { + .mx_UserMenu_contextMenu_displayName { font-weight: bold; font-size: $font-15px; line-height: $font-20px; } - .mx_UserMenuButton_contextMenu_userId { + .mx_UserMenu_contextMenu_userId { font-size: $font-15px; line-height: $font-24px; } } - .mx_UserMenuButton_contextMenu_themeButton { + .mx_UserMenu_contextMenu_themeButton { min-width: 32px; max-width: 32px; width: 32px; @@ -118,31 +150,31 @@ limitations under the License. } } - .mx_UserMenuButton_iconHome::before { + .mx_UserMenu_iconHome::before { mask-image: url('$(res)/img/feather-customised/home.svg'); } - .mx_UserMenuButton_iconBell::before { + .mx_UserMenu_iconBell::before { mask-image: url('$(res)/img/feather-customised/notifications.svg'); } - .mx_UserMenuButton_iconLock::before { + .mx_UserMenu_iconLock::before { mask-image: url('$(res)/img/feather-customised/lock.svg'); } - .mx_UserMenuButton_iconSettings::before { + .mx_UserMenu_iconSettings::before { mask-image: url('$(res)/img/feather-customised/settings.svg'); } - .mx_UserMenuButton_iconArchive::before { + .mx_UserMenu_iconArchive::before { mask-image: url('$(res)/img/feather-customised/archive.svg'); } - .mx_UserMenuButton_iconMessage::before { + .mx_UserMenu_iconMessage::before { mask-image: url('$(res)/img/feather-customised/message-circle.svg'); } - .mx_UserMenuButton_iconSignOut::before { + .mx_UserMenu_iconSignOut::before { mask-image: url('$(res)/img/feather-customised/sign-out.svg'); } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 4731dad1fc..32d6748f94 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -22,18 +22,13 @@ import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList2 from "../views/rooms/RoomList2"; import { Action } from "../../dispatcher/actions"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import BaseAvatar from '../views/avatars/BaseAvatar'; -import UserMenu from "./UserMenuButton"; +import UserMenu from "./UserMenu"; import RoomSearch from "./RoomSearch"; import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { throttle } from 'lodash'; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; /******************************************************************* * CAUTION * @@ -76,32 +71,13 @@ export default class LeftPanel2 extends React.Component { // We watch the middle panel because we don't actually get resized, the middle panel does. // We listen to the noisy channel to avoid choppy reaction times. this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); - - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); - OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } - // TSLint wants this to be a member, but we don't want that. - // tslint:disable-next-line - private onRoomStateUpdate = throttle((ev: MatrixEvent) => { - const myUserId = MatrixClientPeg.get().getUserId(); - if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { - // noinspection JSIgnoredPromiseFromCall - this.onProfileUpdate(); - } - }, 200, {trailing: true, leading: true}); - - private onProfileUpdate = async () => { - // the store triggered an update, so force a layout update. We don't - // have any state to store here for that to magically happen. - this.forceUpdate(); - }; - private onSearch = (term: string): void => { this.setState({searchFilter: term}); }; @@ -170,7 +146,6 @@ export default class LeftPanel2 extends React.Component { // TODO: Presence // TODO: Breadcrumbs toggle // TODO: Menu button - const avatarSize = 32; // should match border-radius of the avatar let breadcrumbs; if (this.state.showBreadcrumbs) { @@ -181,34 +156,9 @@ export default class LeftPanel2 extends React.Component { ); } - let name = {OwnProfileStore.instance.displayName}; - let buttons = ( - - - - ); - if (this.props.isMinimized) { - name = null; - buttons = null; - } - return (
        -
        - - - - {name} - {buttons} -
        + {breadcrumbs}
        ); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index c927e5c7a7..89593aa702 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -35,8 +35,10 @@ import SdkConfig from "../../SdkConfig"; import {getHomePageUrl} from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import BaseAvatar from '../views/avatars/BaseAvatar'; interface IProps { + isMinimized: boolean; } interface IState { @@ -158,14 +160,14 @@ export default class UserMenu extends React.Component { defaultDispatcher.dispatch({action: 'view_home_page'}); }; - public render() { + private renderMenuButton(): React.ReactNode { let contextMenu; if (this.state.menuDisplayed) { let hostingLink; const signupLink = getHostingLink("user-context-menu"); if (signupLink) { hostingLink = ( -
        +
        {_t( "Upgrade to your own domain", {}, { @@ -188,7 +190,7 @@ export default class UserMenu extends React.Component { homeButton = (
      • - + {_t("Home")}
      • @@ -203,18 +205,18 @@ export default class UserMenu extends React.Component { top={elementRect.top + elementRect.height} onFinished={this.onCloseMenu} > -
        -
        -
        - +
        +
        +
        + {OwnProfileStore.instance.displayName} - + {MatrixClientPeg.get().getUserId()}
        @@ -231,31 +233,31 @@ export default class UserMenu extends React.Component { {homeButton}
      • this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - + {_t("Notification settings")}
      • this.onSettingsOpen(e, USER_SECURITY_TAB)}> - + {_t("Security & privacy")}
      • this.onSettingsOpen(e, null)}> - + {_t("All settings")}
      • - + {_t("Archived rooms")}
      • - + {_t("Feedback")}
      • @@ -263,9 +265,9 @@ export default class UserMenu extends React.Component {
          -
        • +
        • - + {_t("Sign out")}
        • @@ -291,4 +293,37 @@ export default class UserMenu extends React.Component { ); } + + public render() { + const avatarSize = 32; // should match border-radius of the avatar + + let name = {OwnProfileStore.instance.displayName}; + let buttons = ( + + {this.renderMenuButton()} + + ); + if (this.props.isMinimized) { + name = null; + buttons = null; + } + + return ( +
          + + + + {name} + {buttons} +
          + ); + } } From 411271422c9f33e802a167074908037ca02a96e9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 19:54:17 -0600 Subject: [PATCH 089/106] Make the whole UserMenu a button to open the menu --- res/css/structures/_LeftPanel2.scss | 10 - res/css/structures/_UserMenu.scss | 76 ++++--- src/components/structures/UserMenu.tsx | 293 +++++++++++++------------ 3 files changed, 193 insertions(+), 186 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 837a1d1f0d..98f23a058b 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -132,16 +132,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_roomListContainer { width: 68px; - .mx_LeftPanel2_userHeader { - .mx_LeftPanel2_headerRow { - justify-content: center; - } - - .mx_LeftPanel2_userAvatarContainer { - margin-right: 0; - } - } - .mx_LeftPanel2_filterContainer { // Organize the flexbox into a centered column layout flex-direction: column; diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index a15eeb6c81..bbb1e1cc7b 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -15,39 +15,7 @@ limitations under the License. */ .mx_UserMenu { - // Create a row-based flexbox to ensure items stay aligned correctly. - display: flex; - align-items: center; - - .mx_UserMenu_userAvatarContainer { - position: relative; // to make default avatars work - margin-right: 8px; - height: 32px; // to remove the unknown 4px gap the browser puts below it - - .mx_UserMenu_userAvatar { - border-radius: 32px; // should match avatar size - } - } - - .mx_UserMenu_userName { - font-weight: 600; - font-size: $font-15px; - line-height: $font-20px; - flex: 1; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - .mx_UserMenu_headerButtons { - // No special styles: the rest of the layout happens to make it work. - } -} - -.mx_UserMenuButton { - > span { width: 16px; height: 16px; position: relative; @@ -67,6 +35,50 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); } } + + .mx_UserMenu_row { + // Create a row-based flexbox to ensure items stay aligned correctly. + display: flex; + align-items: center; + + .mx_UserMenu_userAvatarContainer { + position: relative; // to make default avatars work + margin-right: 8px; + height: 32px; // to remove the unknown 4px gap the browser puts below it + + .mx_UserMenu_userAvatar { + border-radius: 32px; // should match avatar size + } + } + + .mx_UserMenu_userName { + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenu_headerButtons { + // No special styles: the rest of the layout happens to make it work. + } + } + + &.mx_UserMenu_minimized { + .mx_UserMenu_userHeader { + .mx_UserMenu_row { + justify-content: center; + } + + .mx_UserMenu_userAvatarContainer { + margin-right: 0; + } + } + } } .mx_UserMenu_contextMenu { diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 89593aa702..6e3670447e 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -36,6 +36,7 @@ import {getHomePageUrl} from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; +import classNames from "classnames"; interface IProps { isMinimized: boolean; @@ -108,7 +109,9 @@ export default class UserMenu extends React.Component { this.setState({menuDisplayed: true}); }; - private onCloseMenu = () => { + private onCloseMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); this.setState({menuDisplayed: false}); }; @@ -160,147 +163,132 @@ export default class UserMenu extends React.Component { defaultDispatcher.dispatch({action: 'view_home_page'}); }; - private renderMenuButton(): React.ReactNode { - let contextMenu; - if (this.state.menuDisplayed) { - let hostingLink; - const signupLink = getHostingLink("user-context-menu"); - if (signupLink) { - hostingLink = ( -
          - {_t( - "Upgrade to your own domain", {}, - { - a: sub => ( - {sub} - ), - }, - )} -
          - ); - } + private renderContextMenu = (): React.ReactNode => { + if (!this.state.menuDisplayed) return null; - let homeButton = null; - if (this.hasHomePage) { - homeButton = ( -
        • - - - {_t("Home")} - -
        • - ); - } - - const elementRect = this.buttonRef.current.getBoundingClientRect(); - contextMenu = ( - -
          -
          -
          - - {OwnProfileStore.instance.displayName} - - - {MatrixClientPeg.get().getUserId()} - -
          -
          - {_t("Switch -
          -
          - {hostingLink} -
          -
            - {homeButton} -
          • - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - - {_t("Notification settings")} - -
          • -
          • - this.onSettingsOpen(e, USER_SECURITY_TAB)}> - - {_t("Security & privacy")} - -
          • -
          • - this.onSettingsOpen(e, null)}> - - {_t("All settings")} - -
          • -
          • - - - {_t("Archived rooms")} - -
          • -
          • - - - {_t("Feedback")} - -
          • -
          -
          -
          -
            -
          • - - - {_t("Sign out")} - -
          • -
          -
          -
          -
          + let hostingLink; + const signupLink = getHostingLink("user-context-menu"); + if (signupLink) { + hostingLink = ( +
          + {_t( + "Upgrade to your own domain", {}, + { + a: sub => ( + {sub} + ), + }, + )} +
          ); } + let homeButton = null; + if (this.hasHomePage) { + homeButton = ( +
        • + + + {_t("Home")} + +
        • + ); + } + + const elementRect = this.buttonRef.current.getBoundingClientRect(); return ( - - - {/* masked image in CSS */} - - {contextMenu} - + +
          +
          +
          + + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
          +
          + {_t("Switch +
          +
          + {hostingLink} +
          +
            + {homeButton} +
          • + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> + + {_t("Notification settings")} + +
          • +
          • + this.onSettingsOpen(e, USER_SECURITY_TAB)}> + + {_t("Security & privacy")} + +
          • +
          • + this.onSettingsOpen(e, null)}> + + {_t("All settings")} + +
          • +
          • + + + {_t("Archived rooms")} + +
          • +
          • + + + {_t("Feedback")} + +
          • +
          +
          +
          +
            +
          • + + + {_t("Sign out")} + +
          • +
          +
          +
          +
          ); - } + }; public render() { + console.log(this.state); const avatarSize = 32; // should match border-radius of the avatar let name = {OwnProfileStore.instance.displayName}; let buttons = ( - {this.renderMenuButton()} + {/* masked image in CSS */} ); if (this.props.isMinimized) { @@ -308,22 +296,39 @@ export default class UserMenu extends React.Component { buttons = null; } + const classes = classNames({ + 'mx_UserMenu': true, + 'mx_UserMenu_minimized': this.props.isMinimized, + }); + return ( -
          - - - - {name} - {buttons} -
          + + +
          + + + + {name} + {buttons} +
          + {this.renderContextMenu()} +
          +
          + ); } } From 588fea3a9b7d8616b7dd4db18bcd4b817231183a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 19:55:08 -0600 Subject: [PATCH 090/106] Make the menu show up where it was before --- src/components/structures/UserMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 6e3670447e..a824971761 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -204,7 +204,7 @@ export default class UserMenu extends React.Component { return ( From 1888cda5eef0c06e6b70d381515fd3c6e2a0dc1c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 20:22:41 -0600 Subject: [PATCH 091/106] Remove debug --- src/components/structures/UserMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index a824971761..540c8388d1 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -282,7 +282,6 @@ export default class UserMenu extends React.Component { }; public render() { - console.log(this.state); const avatarSize = 32; // should match border-radius of the avatar let name = {OwnProfileStore.instance.displayName}; From 555758d3d2b037256f28c10ba30fa19a0d4ebdd1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 20:23:37 -0600 Subject: [PATCH 092/106] Remove extra space --- src/components/structures/UserMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 540c8388d1..19e57ac51b 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -327,7 +327,6 @@ export default class UserMenu extends React.Component { {this.renderContextMenu()} - ); } } From 7ce3cc1db7f43dafbfa556e44c93974e850b4f18 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Jun 2020 20:35:40 -0600 Subject: [PATCH 093/106] Allow the tag panel to be disabled in the new room list Fixes https://github.com/vector-im/riot-web/issues/14156 --- res/css/structures/_LeftPanel2.scss | 13 ++++++++++++- src/components/structures/LeftPanel2.tsx | 11 ++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 0765b628f6..4bd2f20c89 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -38,6 +38,12 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations // TagPanel handles its own CSS } + &:not(.mx_LeftPanel2_hasTagPanel) { + .mx_LeftPanel2_roomListContainer { + width: 100%; + } + } + // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc .mx_LeftPanel2_roomListContainer { @@ -153,7 +159,12 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations min-width: unset; // We have to forcefully set the width to override the resizer's style attribute. - width: calc(68px + $tagPanelWidth) !important; + &.mx_LeftPanel2_hasTagPanel { + width: calc(68px + $tagPanelWidth) !important; + } + &:not(.mx_LeftPanel2_hasTagPanel) { + width: 68px !important; + } .mx_LeftPanel2_roomListContainer { width: 68px; diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 27583f26ee..9d86fa2b7c 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -34,6 +34,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { throttle } from 'lodash'; import { OwnProfileStore } from "../../stores/OwnProfileStore"; +import SettingsStore from "../../settings/SettingsStore"; /******************************************************************* * CAUTION * @@ -51,10 +52,12 @@ interface IProps { interface IState { searchFilter: string; // TODO: Move search into room list? showBreadcrumbs: boolean; + showTagPanel: boolean; } export default class LeftPanel2 extends React.Component { private listContainerRef: React.RefObject = createRef(); + private tagPanelWatcherRef: string; // TODO: Properly support TagPanel // TODO: Properly support searching/filtering @@ -69,9 +72,13 @@ export default class LeftPanel2 extends React.Component { this.state = { searchFilter: "", showBreadcrumbs: BreadcrumbsStore.instance.visible, + showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { + this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); + }); // We watch the middle panel because we don't actually get resized, the middle panel does. // We listen to the noisy channel to avoid choppy reaction times. @@ -81,6 +88,7 @@ export default class LeftPanel2 extends React.Component { } public componentWillUnmount() { + SettingsStore.unwatchSetting(this.tagPanelWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); @@ -231,7 +239,7 @@ export default class LeftPanel2 extends React.Component { } public render(): React.ReactNode { - const tagPanel = ( + const tagPanel = !this.state.showTagPanel ? null : (
          @@ -252,6 +260,7 @@ export default class LeftPanel2 extends React.Component { const containerClasses = classNames({ "mx_LeftPanel2": true, + "mx_LeftPanel2_hasTagPanel": !!tagPanel, "mx_LeftPanel2_minimized": this.props.isMinimized, }); From 9391d151f363289b9860014c33a5508104a8edfb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 09:15:02 +0100 Subject: [PATCH 094/106] ts-ignore because something is made of fail Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7f838f1e9e..a205a4fb26 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1620,6 +1620,7 @@ export default class MatrixChat extends React.PureComponent { const {hsUrl, isUrl} = this.props.serverConfig; cli = createClient({ baseUrl: hsUrl, + // @ts-ignore - XXX: remove me when it doesn't break tests idBaseUrl: isUrl, }); } From 228a6adfdf17f4b3a8d8c81649f8f7c050280c63 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 09:27:33 +0100 Subject: [PATCH 095/106] indentation --- src/components/views/elements/Spinner.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index ee4351fed6..08ba0cf921 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -34,13 +34,13 @@ const Spinner = ({w = 32, h = 32, imgClassName, message}) => { return (
          { message &&
          { message}
           
          } - +
          ); }; From 274e6f3825cdd0ffcaf67d34bff44f10c2730e15 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 09:35:29 +0100 Subject: [PATCH 096/106] make js-sdk import happy? Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a205a4fb26..9ee8a50c50 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import React, { createRef } from 'react'; -import { createClient } from "matrix-js-sdk/src"; +import * as Matrix from "matrix-js-sdk"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -1618,9 +1618,8 @@ export default class MatrixChat extends React.PureComponent { let cli = MatrixClientPeg.get(); if (!cli) { const {hsUrl, isUrl} = this.props.serverConfig; - cli = createClient({ + cli = Matrix.createClient({ baseUrl: hsUrl, - // @ts-ignore - XXX: remove me when it doesn't break tests idBaseUrl: isUrl, }); } From a905028d3a12c241004cb290d569a044feb1e52b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 09:37:55 +0100 Subject: [PATCH 097/106] bandaid Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9ee8a50c50..7da565739e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -18,6 +18,7 @@ limitations under the License. */ import React, { createRef } from 'react'; +// @ts-ignore - XXX: no idea why this import fails import * as Matrix from "matrix-js-sdk"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; From a2e33a23861da421c21bd5e51eab8d501038a416 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 09:41:18 +0100 Subject: [PATCH 098/106] Prevent old InlineSpinner gif from spinning --- res/css/views/elements/_InlineSpinner.scss | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 561b6cfb82..bdd879b23d 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -18,15 +18,7 @@ limitations under the License. display: inline; } -.mx_InlineSpinner img { +.mx_InlineSpinner_spin img { margin: 0px 6px; vertical-align: -3px; - - animation: spin 1s linear infinite; -} - -@keyframes spin { - 100% { - transform: rotate(360deg); - } -} +} \ No newline at end of file From 96e4b938b2c65d2a87cae1da2c74fa337ce3e47f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 09:42:44 +0100 Subject: [PATCH 099/106] Don't modify the size of the MessagePanel spinner --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 481741dfd2..d11fee6360 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -770,7 +770,7 @@ export default class MessagePanel extends React.Component { topSpinner =
        • ; } if (this.props.forwardPaginating) { - bottomSpinner =
        • ; + bottomSpinner =
        • ; } const style = this.props.hidden ? { display: 'none' } : {}; From e790a31f09e09c9c3046416ff72cd06e8829aa30 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 26 Jun 2020 09:44:47 +0100 Subject: [PATCH 100/106] Include newline at end of _InlineSpinner.scss --- res/css/views/elements/_InlineSpinner.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index bdd879b23d..6b91e45923 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -21,4 +21,4 @@ limitations under the License. .mx_InlineSpinner_spin img { margin: 0px 6px; vertical-align: -3px; -} \ No newline at end of file +} From c8381021b823dcd71c0e057756231daec8185306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=95=20Tim?= Date: Sun, 28 Jun 2020 04:08:06 +0800 Subject: [PATCH 101/106] Hide room list show less button if it would do nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ☕ Tim On the new rooms list, if the show less button wouldn't result in a smaller list, don't show it. Fixes vector-im/riot-web#14219 --- src/components/views/rooms/RoomSublist2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 79efa565b7..d34ddf090d 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -393,7 +393,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText}
        ); - } else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) { + } else if (tiles.length <= nVisible && tiles.length > this.props.layout.defaultVisibleTiles) { // we have all tiles visible - add a button to show less let showLessText = ( From 753f7aa5b8d63c7fee85d127fd39b98f3969cebd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 27 Jun 2020 22:48:54 -0600 Subject: [PATCH 102/106] Remove the DM button from new room tiles Fixes https://github.com/vector-im/riot-web/issues/14221 --- res/css/views/rooms/_RoomTile2.scss | 4 ---- src/components/views/rooms/RoomTile2.tsx | 13 ++----------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index a97d1fd5b9..2b659a0c8e 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -193,10 +193,6 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/arrow-down.svg'); } - .mx_RoomTile2_iconUser::before { - mask-image: url('$(res)/img/feather-customised/user.svg'); - } - .mx_RoomTile2_iconSettings::before { mask-image: url('$(res)/img/feather-customised/settings.svg'); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 3d0a555877..2791bd9730 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -135,11 +135,8 @@ export default class RoomTile2 extends React.Component { ev.preventDefault(); ev.stopPropagation(); - if (tagId === DefaultTagID.DM) { - // TODO: DM Flagging - } else { - // TODO: XOR favourites and low priority - } + // TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211 + // TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210 }; private onLeaveRoomClick = (ev: ButtonEvent) => { @@ -196,12 +193,6 @@ export default class RoomTile2 extends React.Component { {_t("Low Priority")} -
      • - this.onTagRoom(e, DefaultTagID.DM)}> - - {_t("Direct Chat")} - -
      • From 62467144ba48ac256f0c849a98f8f3c799d8ed1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 28 Jun 2020 20:03:55 -0600 Subject: [PATCH 103/106] Update i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d721979329..17943a61c7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1220,7 +1220,6 @@ "Unread messages.": "Unread messages.", "Favourite": "Favourite", "Low Priority": "Low Priority", - "Direct Chat": "Direct Chat", "Leave Room": "Leave Room", "Room options": "Room options", "Add a topic": "Add a topic", @@ -1898,6 +1897,7 @@ "Mentions only": "Mentions only", "Leave": "Leave", "Forget": "Forget", + "Direct Chat": "Direct Chat", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", From 518db90b6981bb6775fb211461552140f7c93ce3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 29 Jun 2020 13:55:06 +0100 Subject: [PATCH 104/106] Support accounts with cross signing but no SSSS Port https://github.com/matrix-org/matrix-react-sdk/pull/4717 to release --- src/components/structures/MatrixChat.tsx | 53 ++++++++----------- .../structures/auth/Registration.js | 7 +-- .../structures/auth/SetupEncryptionBody.js | 27 ++++++++-- src/i18n/strings/en_EN.json | 3 +- src/stores/SetupEncryptionStore.js | 30 +++++++++-- 5 files changed, 79 insertions(+), 41 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 634e13b103..9cb6ed078c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1870,42 +1870,35 @@ export default class MatrixChat extends React.PureComponent { this.accountPasswordTimer = null; }, 60 * 5 * 1000); - // Wait for the client to be logged in (but not started) - // which is enough to ask the server about account data. - const loggedIn = new Promise(resolve => { - const actionHandlerRef = dis.register(payload => { - if (payload.action !== "on_logged_in") { - return; - } - dis.unregister(actionHandlerRef); - resolve(); - }); - }); - - // Create and start the client in the background - const setLoggedInPromise = Lifecycle.setLoggedIn(credentials); - await loggedIn; + // Create and start the client + await Lifecycle.setLoggedIn(credentials); const cli = MatrixClientPeg.get(); - // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` - // because the client hasn't been started yet. - const cryptoAvailable = isCryptoAvailable(); - if (!cryptoAvailable) { + const cryptoEnabled = cli.isCryptoEnabled(); + if (!cryptoEnabled) { this.onLoggedIn(); } - this.setState({ pendingInitialSync: true }); - await this.firstSyncPromise.promise; - - if (!cryptoAvailable) { - this.setState({ pendingInitialSync: false }); - return setLoggedInPromise; + const promisesList = [this.firstSyncPromise.promise]; + if (cryptoEnabled) { + // wait for the client to finish downloading cross-signing keys for us so we + // know whether or not we have keys set up on this account + promisesList.push(cli.downloadKeys([cli.getUserId()])); } - // Test for the master cross-signing key in SSSS as a quick proxy for - // whether cross-signing has been set up on the account. - const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master"); - if (masterKeyInStorage) { + // Now update the state to say we're waiting for the first sync to complete rather + // than for the login to finish. + this.setState({ pendingInitialSync: true }); + + await Promise.all(promisesList); + + if (!cryptoEnabled) { + this.setState({ pendingInitialSync: false }); + return; + } + + const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); + if (crossSigningIsSetUp) { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); @@ -1913,8 +1906,6 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); - - return setLoggedInPromise; }; // complete security / e2e setup has finished diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6349614d72..3b5f5676dc 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -378,7 +378,7 @@ export default createReactClass({ } if (response.access_token) { - const cli = await this.props.onLoggedIn({ + await this.props.onLoggedIn({ userId: response.user_id, deviceId: response.device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), @@ -386,7 +386,7 @@ export default createReactClass({ accessToken: response.access_token, }, this.state.formVals.password); - this._setupPushers(cli); + this._setupPushers(); // we're still busy until we get unmounted: don't show the registration form again newState.busy = true; } else { @@ -397,10 +397,11 @@ export default createReactClass({ this.setState(newState); }, - _setupPushers: function(matrixClient) { + _setupPushers: function() { if (!this.props.brand) { return Promise.resolve(); } + const matrixClient = MatrixClientPeg.get(); return matrixClient.getPushers().then((resp)=>{ const pushers = resp.pushers; for (let i = 0; i < pushers.length; ++i) { diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 26534c6e02..f2e702c8cb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -28,6 +28,14 @@ import { PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; +function keyHasPassphrase(keyInfo) { + return ( + keyInfo.passphrase && + keyInfo.passphrase.salt && + keyInfo.passphrase.iterations + ); +} + export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, @@ -108,6 +116,21 @@ export default class SetupEncryptionBody extends React.Component { member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; } else if (phase === PHASE_INTRO) { + const store = SetupEncryptionStore.sharedInstance(); + let recoveryKeyPrompt; + if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { + recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); + } else if (store.keyInfo) { + recoveryKeyPrompt = _t("Use Recovery Key"); + } + + let useRecoveryKeyButton; + if (recoveryKeyPrompt) { + useRecoveryKeyButton = + {recoveryKeyPrompt} + ; + } + return (

        {_t( @@ -131,9 +154,7 @@ export default class SetupEncryptionBody extends React.Component {

        - - {_t("Use Recovery Passphrase or Key")} - + {useRecoveryKeyButton} {_t("Skip")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9a41517664..e927f0202b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2121,10 +2121,11 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", + "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", + "Use Recovery Key": "Use Recovery Key", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", - "Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index ae1f998b02..b55b5c223c 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -36,11 +36,20 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_INTRO; + this.phase = PHASE_BUSY; this.verificationRequest = null; this.backupInfo = null; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); - MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + + // ID of the key that the secrets we want are encrypted with + this.keyId = null; + // Descriptor of the key that the secrets we want are encrypted with + this.keyInfo = null; + + const cli = MatrixClientPeg.get(); + cli.on("crypto.verification.request", this.onVerificationRequest); + cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged); + + this.fetchKeyInfo(); } stop() { @@ -57,6 +66,21 @@ export class SetupEncryptionStore extends EventEmitter { } } + async fetchKeyInfo() { + const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); + if (keys === null || Object.keys(keys).length === 0) { + this.keyId = null; + this.keyInfo = null; + } else { + // If the secret is stored under more than one key, we just pick an arbitrary one + this.keyId = Object.keys(keys)[0]; + this.keyInfo = keys[this.keyId]; + } + + this.phase = PHASE_INTRO; + this.emit("update"); + } + async usePassPhrase() { this.phase = PHASE_BUSY; this.emit("update"); From 9329be3bb894ecae689c56d2662795dc34ee6ade Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 29 Jun 2020 15:59:36 +0100 Subject: [PATCH 105/106] Prepare changelog for v2.8.1 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21bfbf0a93..aa476ae6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [2.8.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.1) (2020-06-29) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0...v2.8.1) + + * Support accounts with cross signing but no SSSS + [\#4852](https://github.com/matrix-org/matrix-react-sdk/pull/4852) + Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0) (2020-06-23) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) From e43311c0f468c56e2a361c6f1c633fce348b5776 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 29 Jun 2020 15:59:37 +0100 Subject: [PATCH 106/106] v2.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 591922498f..c623c64e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.8.0", + "version": "2.8.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": {