From e04490284d1d0be35f446c485c3273556ebed751 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 21:50:25 -0600 Subject: [PATCH 01/59] Support UI for MSC2876: Widgets reading events from rooms MSC: https://github.com/matrix-org/matrix-doc/pull/2876 Fixes https://github.com/vector-im/element-web/issues/15747 Requires https://github.com/matrix-org/matrix-widget-api/pull/34 --- src/i18n/strings/en_EN.json | 13 +++++++ src/stores/widgets/StopGapWidgetDriver.ts | 45 +++++++++++++++++++++++ src/widgets/CapabilityText.tsx | 20 ++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5863f2a834..8a240264c0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,20 +598,33 @@ "Change which room, message, or user you're viewing": "Change which room, message, or user you're viewing", "Change the topic of this room": "Change the topic of this room", "See when the topic changes in this room": "See when the topic changes in this room", + "See the current topic for this room": "See the current topic for this room", "Change the topic of your active room": "Change the topic of your active room", "See when the topic changes in your active room": "See when the topic changes in your active room", + "See the current topic in your active room": "See the current topic in your active room", "Change the name of this room": "Change the name of this room", "See when the name changes in this room": "See when the name changes in this room", + "See the current name for this room": "See the current name for this room", "Change the name of your active room": "Change the name of your active room", "See when the name changes in your active room": "See when the name changes in your active room", + "See the current name of your active room": "See the current name of your active room", "Change the avatar of this room": "Change the avatar of this room", "See when the avatar changes in this room": "See when the avatar changes in this room", + "See the current avatar for this room": "See the current avatar for this room", "Change the avatar of your active room": "Change the avatar of your active room", "See when the avatar changes in your active room": "See when the avatar changes in your active room", + "See the current avatar for your active room": "See the current avatar for your active room", + "Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave", + "See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room", + "See the membership status of anyone in this room": "See the membership status of anyone in this room", + "Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave", + "See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room", "Send stickers to this room as you": "Send stickers to this room as you", "See when a sticker is posted in this room": "See when a sticker is posted in this room", + "See recent stickers posted to this room": "See recent stickers posted to this room", "Send stickers to your active room as you": "Send stickers to your active room as you", "See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room", + "See recent stickers posted to your active room": "See recent stickers posted to your active room", "with an empty state key": "with an empty state key", "with state key %(stateKey)s": "with state key %(stateKey)s", "Send %(eventType)s events as you in this room": "Send %(eventType)s events as you in this room", diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 8a286d909b..7d1c3f3791 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -44,6 +44,7 @@ import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; // TODO: Purge this from the universe @@ -144,6 +145,50 @@ export class StopGapWidgetDriver extends WidgetDriver { return {roomId, eventId: r.event_id}; } + public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { + limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice + + const client = MatrixClientPeg.get(); + const roomId = ActiveRoomObserver.activeRoomId; + const room = client.getRoom(roomId); + if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + + const results: MatrixEvent[] = []; + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i > 0; i--) { + if (results.length >= limit) break; + + const ev = events[i]; + if (ev.getType() !== eventType) continue; + if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; + results.push(ev); + } + + return results.map(e => e.event); + } + + public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { + limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice + + const client = MatrixClientPeg.get(); + const roomId = ActiveRoomObserver.activeRoomId; + const room = client.getRoom(roomId); + if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + + const results: MatrixEvent[] = []; + const state = room.currentState.events.get(eventType); + if (state) { + if (stateKey === "" || !!stateKey) { + const forKey = state.get(stateKey); + if (forKey) results.push(forKey); + } else { + results.push(...Array.from(state.values())); + } + } + + return results.slice(0, limit).map(e => e.event); + } + public async askOpenID(observer: SimpleObservable) { const oidcState = WidgetPermissionStore.instance.getOIDCState( this.forWidget, this.forWidgetKind, this.inRoomId, diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 273d22dc81..f7ff94947d 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -70,30 +70,48 @@ export class CapabilityText { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the topic of this room"), [EventDirection.Receive]: _td("See when the topic changes in this room"), + [EventDirection.Read]: _td("See the current topic for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the topic of your active room"), [EventDirection.Receive]: _td("See when the topic changes in your active room"), + [EventDirection.Read]: _td("See the current topic in your active room"), }, }, [EventType.RoomName]: { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the name of this room"), [EventDirection.Receive]: _td("See when the name changes in this room"), + [EventDirection.Read]: _td("See the current name for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the name of your active room"), [EventDirection.Receive]: _td("See when the name changes in your active room"), + [EventDirection.Read]: _td("See the current name of your active room"), }, }, [EventType.RoomAvatar]: { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the avatar of this room"), [EventDirection.Receive]: _td("See when the avatar changes in this room"), + [EventDirection.Read]: _td("See the current avatar for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the avatar of your active room"), [EventDirection.Receive]: _td("See when the avatar changes in your active room"), + [EventDirection.Read]: _td("See the current avatar for your active room"), + }, + }, + [EventType.RoomMember]: { + [WidgetKind.Room]: { + [EventDirection.Send]: _td("Kick, ban, or invite people to this room, and make you leave"), + [EventDirection.Receive]: _td("See when people join, leave, or are invited to this room"), + [EventDirection.Read]: _td("See the membership status of anyone in this room"), + }, + [GENERIC_WIDGET_KIND]: { + [EventDirection.Send]: _td("Kick, ban, or invite people to your active room, and make you leave"), + [EventDirection.Receive]: _td("See when people join, leave, or are invited to your active room"), + [EventDirection.Read]: _td("See the membership status of anyone in this room"), }, }, }; @@ -103,10 +121,12 @@ export class CapabilityText { [WidgetKind.Room]: { [EventDirection.Send]: _td("Send stickers to this room as you"), [EventDirection.Receive]: _td("See when a sticker is posted in this room"), + [EventDirection.Read]: _td("See recent stickers posted to this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Send stickers to your active room as you"), [EventDirection.Receive]: _td("See when anyone posts a sticker to your active room"), + [EventDirection.Read]: _td("See recent stickers posted to your active room"), }, }, }; From 903cc77f3940190ffe64cddc6d9bc9b1271e3dd6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 21:53:23 -0600 Subject: [PATCH 02/59] Appease the linter --- src/stores/widgets/StopGapWidgetDriver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 7d1c3f3791..25e81c47a2 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -167,7 +167,9 @@ export class StopGapWidgetDriver extends WidgetDriver { return results.map(e => e.event); } - public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { + public async readStateEvents( + eventType: string, stateKey: string | undefined, limit: number, + ): Promise { limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice const client = MatrixClientPeg.get(); From 965b0ab58d0dceebff227ce895fff498cba9a1d7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 May 2021 16:28:00 +0100 Subject: [PATCH 03/59] Upgrade matrix-js-sdk to 11.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 d9ea440936..4f6de8ce5c 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "11.0.0-rc.1", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 7712ac507a..4fe12f3768 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,9 +5669,10 @@ mathml-tag-names@^2.1.3: 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 "10.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a" +matrix-js-sdk@11.0.0-rc.1: + version "11.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.0.0-rc.1.tgz#a40fdc9e93a1a9e873dd867e5766a20ea780e1d8" + integrity sha512-zATTKWH4yrs6mKcLQNvhvwi1IRGUEx3xt3MPCFuSuQq23TMbnxhOZMMpUF6n19FpioHw9jbz7cPsdVKM2gpDLw== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 657481156f33d456a0e53bf061adf2e36bcf4e4d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 May 2021 16:36:52 +0100 Subject: [PATCH 04/59] Prepare changelog for v3.21.0-rc.1 --- CHANGELOG.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d23e3413..fe714b2b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,89 @@ +Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1) + + * Upgrade to JS SDK 11.0.0-rc.1 + * Add disclaimer about subspaces being experimental in add existing dialog + [\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978) + * Spaces Beta release + [\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933) + * Improve permissions error when adding new server to room directory + [\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009) + * Allow user to progress through space creation & setup using Enter + [\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006) + * Upgrade sanitize types + [\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008) + * Upgrade `cheerio` and resolve type errors + [\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007) + * Add slash commands support to edit message composer + [\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865) + * Fix the two todays problem + [\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940) + * Switch the Home Space out for an All rooms space + [\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969) + * Show device ID in UserInfo when there is no device name + [\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985) + * Switch back to release version of `sanitize-html` + [\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005) + * Bump hosted-git-info from 2.8.8 to 2.8.9 + [\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998) + * Don't use the event's metadata to calc the scale of an image + [\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982) + * Adjust MIME type of upload confirmation if needed + [\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981) + * Forbid redaction of encryption events + [\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991) + * Fix voice message playback being squished up against send button + [\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988) + * Improve style of notification badges on the space panel + [\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983) + * Add dev dependency for parse5 typings + [\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990) + * Iterate Spaces admin UX around room management + [\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977) + * Guard all isSpaceRoom calls behind the labs flag + [\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979) + * Bump lodash from 4.17.20 to 4.17.21 + [\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986) + * Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests + [\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987) + * Bump ua-parser-js from 0.7.23 to 0.7.28 + [\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984) + * Update visual style of plain files in the timeline + [\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971) + * Support for multiple streams (not MSC3077) + [\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833) + * Update space ordering behaviour to match updates in MSC + [\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963) + * Improve performance of search all spaces and space switching + [\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976) + * Update colours and sizing for voice messages + [\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970) + * Update link to Android SDK + [\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973) + * Add cleanup functions for image view + [\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962) + * Add a note about sharing your IP in P2P calls + [\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961) + * Only aggregate DM notifications on the Space Panel in the Home Space + [\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968) + * Add retry mechanism and progress bar to add existing to space dialog + [\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975) + * Warn on access token reveal + [\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755) + * Fix newly joined room appearing under the wrong space + [\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945) + * Early rendering for voice messages in the timeline + [\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955) + * Calculate the real waveform in the Playback class for voice messages + [\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956) + * Don't recurse on arrayFastResample + [\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957) + * Support a dark theme for voice messages + [\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958) + * Handle no/blocked microphones in voice messages + [\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959) + Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0) From 392505e7a1447a1bf1c5fd7acbc5eed0528fce68 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 May 2021 16:36:53 +0100 Subject: [PATCH 05/59] v3.21.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4f6de8ce5c..a7fd78b903 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.20.0", + "version": "3.21.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -197,5 +197,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 0717df71c22ae9ba2cddba4cefe1394fa95e9ab9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 May 2021 12:32:39 +0100 Subject: [PATCH 06/59] Add missing space on beta feedback dialog --- src/components/views/dialogs/BetaFeedbackDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 119799c609..6619f11b33 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -63,7 +63,7 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { description={
{ _t(info.feedbackSubheading) } - +   { _t("Your platform and username will be noted to help us use your feedback as much as we can.")} { From 654ce95c743fe1151d59905d7f362c999e5a5a8d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 12 May 2021 17:31:55 +0100 Subject: [PATCH 07/59] Progress from adding existing rooms to new space upon completion --- src/components/structures/SpaceRoomView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index ed0ae1afe7..828b2ff5fe 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -546,6 +546,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { } } setBusy(false); + onFinished(); }; buttonLabel = busy ? _t("Adding...") : _t("Add"); } From 3aef3b72b5a3897e6b99a5931ba09cc9b39c51ea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 May 2021 14:08:32 -0600 Subject: [PATCH 08/59] Move language handling into languageHandler --- src/components/views/elements/LanguageDropdown.js | 9 ++------- src/languageHandler.tsx | 9 +++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index b8734c5afb..9420061a74 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. - const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); - if (language) { - this.props.onOptionChange(language); - } else { - const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); - this.props.onOptionChange(language); - } + const language = languageHandler.getUserLanguage(); + this.props.onOptionChange(language); } } diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index b61f57d4b3..f30e735f03 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -56,6 +56,15 @@ export function newTranslatableError(message: string) { return error; } +export function getUserLanguage(): string { + const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); + if (language) { + return language; + } else { + return normalizeLanguageKey(getLanguageFromBrowser()); + } +} + // Function which only purpose is to mark that a string is translatable // Does not actually do anything. It's helpful for automatic extraction of translatable strings export function _td(s: string): string { From f98eee318e5cb70bd51e0644c73d67741e560475 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 May 2021 14:10:02 -0600 Subject: [PATCH 09/59] Fill out fields for MSC2873 values As required by https://github.com/matrix-org/matrix-widget-api/pull/36 --- .../views/dialogs/ModalWidgetDialog.tsx | 9 +++++++-- src/identifiers.ts | 17 +++++++++++++++++ src/stores/widgets/StopGapWidget.ts | 7 ++++++- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/identifiers.ts diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 59eaab7b81..0c474b160c 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 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. @@ -16,7 +16,7 @@ limitations under the License. import * as React from 'react'; import BaseDialog from './BaseDialog'; -import { _t } from '../../../languageHandler'; +import { _t, getUserLanguage } from '../../../languageHandler'; import AccessibleButton from "../elements/AccessibleButton"; import { ClientWidgetApi, @@ -39,6 +39,8 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore"; import { arrayFastClone } from "../../../utils/arrays"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {ELEMENT_CLIENT_ID} from "../../../identifiers"; +import SettingsStore from "../../../settings/SettingsStore"; interface IProps { widgetDefinition: IModalWidgetOpenRequestData; @@ -129,6 +131,9 @@ export default class ModalWidgetDialog extends React.PureComponent Date: Wed, 12 May 2021 21:15:17 -0600 Subject: [PATCH 10/59] Try putting room list handling behind a lock Some of the logs relating to room list corruption appear to be badly timed race conditions so we'll try to linearize them here. --- src/stores/room-list/algorithms/Algorithm.ts | 315 ++++++++++--------- src/utils/MultiLock.ts | 30 ++ 2 files changed, 193 insertions(+), 152 deletions(-) create mode 100644 src/utils/MultiLock.ts diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 024c484c41..395591d321 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import SettingsStore from "../../../settings/SettingsStore"; import { VisibilityProvider } from "../filters/VisibilityProvider"; +import {MultiLock} from "../../../utils/MultiLock"; /** * Fired when the Algorithm has determined a list has been updated. @@ -77,6 +78,7 @@ export class Algorithm extends EventEmitter { } = {}; private allowedByFilter: Map = new Map(); private allowedRoomsByFilters: Set = new Set(); + private handlerLock = new MultiLock(); /** * Set to true to suspend emissions of algorithm updates. @@ -679,191 +681,200 @@ export class Algorithm extends EventEmitter { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); + console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`); } - if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); - - // Note: check the isSticky against the room ID just in case the reference is wrong - const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; - if (cause === RoomUpdateCause.NewRoom) { - const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room; - const roomTags = this.roomIdsToTags[room.roomId]; - const hasTags = roomTags && roomTags.length > 0; - - // Don't change the cause if the last sticky room is being re-added. If we fail to - // pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus - // lose the room. - if (hasTags && !isForLastSticky) { - console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`); - cause = RoomUpdateCause.PossibleTagChange; + const release = await this.handlerLock.acquire(room.roomId); + try { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); } + if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); - // Check to see if the room is known first - let knownRoomRef = this.rooms.includes(room); - if (hasTags && !knownRoomRef) { - console.warn(`${room.roomId} might be a reference change - attempting to update reference`); - this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r); - knownRoomRef = this.rooms.includes(room); - if (!knownRoomRef) { - console.warn(`${room.roomId} is still not referenced. It may be sticky.`); + // Note: check the isSticky against the room ID just in case the reference is wrong + const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; + if (cause === RoomUpdateCause.NewRoom) { + const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room; + const roomTags = this.roomIdsToTags[room.roomId]; + const hasTags = roomTags && roomTags.length > 0; + + // Don't change the cause if the last sticky room is being re-added. If we fail to + // pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus + // lose the room. + if (hasTags && !isForLastSticky) { + console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`); + cause = RoomUpdateCause.PossibleTagChange; + } + + // Check to see if the room is known first + let knownRoomRef = this.rooms.includes(room); + if (hasTags && !knownRoomRef) { + console.warn(`${room.roomId} might be a reference change - attempting to update reference`); + this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r); + knownRoomRef = this.rooms.includes(room); + if (!knownRoomRef) { + console.warn(`${room.roomId} is still not referenced. It may be sticky.`); + } + } + + // If we have tags for a room and don't have the room referenced, something went horribly + // wrong - the reference should have been updated above. + if (hasTags && !knownRoomRef && !isSticky) { + throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); + } + + // Like above, update the reference to the sticky room if we need to + if (hasTags && isSticky) { + // Go directly in and set the sticky room's new reference, being careful not + // to trigger a sticky room update ourselves. + this._stickyRoom.room = room; + } + + // If after all that we're still a NewRoom update, add the room if applicable. + // We don't do this for the sticky room (because it causes duplication issues) + // or if we know about the reference (as it should be replaced). + if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) { + this.rooms.push(room); } } - // If we have tags for a room and don't have the room referenced, something went horribly - // wrong - the reference should have been updated above. - if (hasTags && !knownRoomRef && !isSticky) { - throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); - } + let didTagChange = false; + if (cause === RoomUpdateCause.PossibleTagChange) { + const oldTags = this.roomIdsToTags[room.roomId] || []; + const newTags = this.getTagsForRoom(room); + const diff = arrayDiff(oldTags, newTags); + if (diff.removed.length > 0 || diff.added.length > 0) { + for (const rmTag of diff.removed) { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Removing ${room.roomId} from ${rmTag}`); + } + const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; + if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); + await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); + this._cachedRooms[rmTag] = algorithm.orderedRooms; + this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list + this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed + } + for (const addTag of diff.added) { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Adding ${room.roomId} to ${addTag}`); + } + const algorithm: OrderingAlgorithm = this.algorithms[addTag]; + if (!algorithm) throw new Error(`No algorithm for ${addTag}`); + await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); + this._cachedRooms[addTag] = algorithm.orderedRooms; + } - // Like above, update the reference to the sticky room if we need to - if (hasTags && isSticky) { - // Go directly in and set the sticky room's new reference, being careful not - // to trigger a sticky room update ourselves. - this._stickyRoom.room = room; - } + // Update the tag map so we don't regen it in a moment + this.roomIdsToTags[room.roomId] = newTags; - // If after all that we're still a NewRoom update, add the room if applicable. - // We don't do this for the sticky room (because it causes duplication issues) - // or if we know about the reference (as it should be replaced). - if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) { - this.rooms.push(room); - } - } - - let didTagChange = false; - if (cause === RoomUpdateCause.PossibleTagChange) { - const oldTags = this.roomIdsToTags[room.roomId] || []; - const newTags = this.getTagsForRoom(room); - const diff = arrayDiff(oldTags, newTags); - if (diff.removed.length > 0 || diff.added.length > 0) { - for (const rmTag of diff.removed) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Removing ${room.roomId} from ${rmTag}`); + console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); } - const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; - if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); - this._cachedRooms[rmTag] = algorithm.orderedRooms; - this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list - this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed - } - for (const addTag of diff.added) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Adding ${room.roomId} to ${addTag}`); - } - const algorithm: OrderingAlgorithm = this.algorithms[addTag]; - if (!algorithm) throw new Error(`No algorithm for ${addTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); - this._cachedRooms[addTag] = algorithm.orderedRooms; - } - - // Update the tag map so we don't regen it in a moment - this.roomIdsToTags[room.roomId] = newTags; - - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); - } - cause = RoomUpdateCause.Timeline; - didTagChange = true; - } else { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); - } - cause = RoomUpdateCause.Timeline; - } - - if (didTagChange && isSticky) { - // Manually update the tag for the sticky room without triggering a sticky room - // update. The update will be handled implicitly by the sticky room handling and - // requires no changes on our part, if we're in the middle of a sticky room change. - if (this._lastStickyRoom) { - this._stickyRoom = { - room, - tag: this.roomIdsToTags[room.roomId][0], - position: 0, // right at the top as it changed tags - }; + cause = RoomUpdateCause.Timeline; + didTagChange = true; } else { - // We have to clear the lock as the sticky room change will trigger updates. - await this.setStickyRoom(room); + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); + } + cause = RoomUpdateCause.Timeline; + } + + if (didTagChange && isSticky) { + // Manually update the tag for the sticky room without triggering a sticky room + // update. The update will be handled implicitly by the sticky room handling and + // requires no changes on our part, if we're in the middle of a sticky room change. + if (this._lastStickyRoom) { + this._stickyRoom = { + room, + tag: this.roomIdsToTags[room.roomId][0], + position: 0, // right at the top as it changed tags + }; + } else { + // We have to clear the lock as the sticky room change will trigger updates. + await this.setStickyRoom(room); + } } } - } - // If the update is for a room change which might be the sticky room, prevent it. We - // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though - // as the sticky room relies on this. - if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { - if (this.stickyRoom === room) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); + // If the update is for a room change which might be the sticky room, prevent it. We + // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though + // as the sticky room relies on this. + if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { + if (this.stickyRoom === room) { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); + } + return false; } - return false; } - } - if (!this.roomIdsToTags[room.roomId]) { - if (CAUSES_REQUIRING_ROOM.includes(cause)) { + if (!this.roomIdsToTags[room.roomId]) { + if (CAUSES_REQUIRING_ROOM.includes(cause)) { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); + } + return false; + } + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); + console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); + } + + // Get the tags for the room and populate the cache + const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); + + // "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(), + // which means we should *always* have a tag to go off of. + if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`); + + this.roomIdsToTags[room.roomId] = roomTags; + + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); } - return false; } if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); + console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); } - // Get the tags for the room and populate the cache - const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } - // "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(), - // which means we should *always* have a tag to go off of. - if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`); + let changed = didTagChange; + for (const tag of tags) { + const algorithm: OrderingAlgorithm = this.algorithms[tag]; + if (!algorithm) throw new Error(`No algorithm for ${tag}`); - this.roomIdsToTags[room.roomId] = roomTags; + await algorithm.handleRoomUpdate(room, cause); + this._cachedRooms[tag] = algorithm.orderedRooms; + + // Flag that we've done something + this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list + this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed + changed = true; + } if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); + console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); } + return changed; + } finally { + release(); } - - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); - } - - const tags = this.roomIdsToTags[room.roomId]; - if (!tags) { - console.warn(`No tags known for "${room.name}" (${room.roomId})`); - return false; - } - - let changed = didTagChange; - for (const tag of tags) { - const algorithm: OrderingAlgorithm = this.algorithms[tag]; - if (!algorithm) throw new Error(`No algorithm for ${tag}`); - - await algorithm.handleRoomUpdate(room, cause); - this._cachedRooms[tag] = algorithm.orderedRooms; - - // Flag that we've done something - this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list - this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed - changed = true; - } - - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); - } - return changed; } } diff --git a/src/utils/MultiLock.ts b/src/utils/MultiLock.ts new file mode 100644 index 0000000000..97cc30306a --- /dev/null +++ b/src/utils/MultiLock.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 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 {EnhancedMap} from "./maps"; +import AwaitLock from "await-lock"; + +export type DoneFn = () => void; + +export class MultiLock { + private locks = new EnhancedMap(); + + public async acquire(key: string): Promise { + const lock = this.locks.getOrCreate(key, new AwaitLock()); + await lock.acquireAsync(); + return () => lock.release(); + } +} From deab424c935fa9949ab0b9b8723b192ce072b0f4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 May 2021 21:19:31 -0600 Subject: [PATCH 11/59] Appease the linter --- src/stores/room-list/algorithms/Algorithm.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 395591d321..207035ffce 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -720,7 +720,7 @@ export class Algorithm extends EventEmitter { // If we have tags for a room and don't have the room referenced, something went horribly // wrong - the reference should have been updated above. if (hasTags && !knownRoomRef && !isSticky) { - throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); + throw new Error(`${room.roomId} is missing from room array but is known`); } // Like above, update the reference to the sticky room if we need to @@ -808,7 +808,9 @@ export class Algorithm extends EventEmitter { if (this.stickyRoom === room) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); + console.warn( + `[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`, + ); } return false; } @@ -870,7 +872,9 @@ export class Algorithm extends EventEmitter { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); + console.log( + `[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`, + ); } return changed; } finally { From 423c515708b7cbc50ad1139a50d259501057bd6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 13 May 2021 09:46:45 +0100 Subject: [PATCH 12/59] Consolidate AddExistingToSpace between Dialog and Just Me integrated flow --- .../dialogs/_AddExistingToSpaceDialog.scss | 168 +++++----- src/components/structures/SpaceRoomView.tsx | 59 +--- .../dialogs/AddExistingToSpaceDialog.tsx | 289 +++++++++--------- 3 files changed, 243 insertions(+), 273 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 91947be76a..575faf4a97 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -105,6 +105,90 @@ limitations under the License. mask-position: center; } } + + .mx_AddExistingToSpace_footer { + display: flex; + margin-top: 20px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_AddExistingToSpace_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + } + + > * { + vertical-align: middle; + } + } + + .mx_AddExistingToSpace_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpace_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpace_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpace_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } } .mx_AddExistingToSpaceDialog { @@ -189,88 +273,4 @@ limitations under the License. .mx_AddExistingToSpace { display: contents; } - - .mx_AddExistingToSpaceDialog_footer { - display: flex; - margin-top: 20px; - - > span { - flex-grow: 1; - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - - .mx_ProgressBar { - height: 8px; - width: 100%; - - @mixin ProgressBarBorderRadius 8px; - } - - .mx_AddExistingToSpaceDialog_progressText { - margin-top: 8px; - font-size: $font-15px; - line-height: $font-24px; - color: $primary-fg-color; - } - - > * { - vertical-align: middle; - } - } - - .mx_AddExistingToSpaceDialog_error { - padding-left: 12px; - - > img { - align-self: center; - } - - .mx_AddExistingToSpaceDialog_errorHeading { - font-weight: $font-semi-bold; - font-size: $font-15px; - line-height: $font-18px; - color: $notice-primary-color; - } - - .mx_AddExistingToSpaceDialog_errorCaption { - margin-top: 4px; - font-size: $font-12px; - line-height: $font-15px; - color: $primary-fg-color; - } - } - - .mx_AccessibleButton { - display: inline-block; - align-self: center; - } - - .mx_AccessibleButton_kind_primary { - padding: 8px 36px; - } - - .mx_AddExistingToSpaceDialog_retryButton { - margin-left: 12px; - padding-left: 24px; - position: relative; - - &::before { - content: ''; - position: absolute; - background-color: $primary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/retry.svg'); - width: 18px; - height: 18px; - left: 0; - } - } - - .mx_AccessibleButton_kind_link { - padding: 0; - } - } } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 828b2ff5fe..4c4b076fdd 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -517,40 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { }; const SpaceAddExistingRooms = ({ space, onFinished }) => { - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - - let onClick = onFinished; - let buttonLabel = _t("Skip for now"); - if (selectedToAdd.size > 0) { - onClick = async () => { - setBusy(true); - - for (const room of selectedToAdd) { - const via = calculateRoomVia(room); - try { - await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { - if (e.errcode === "M_LIMIT_EXCEEDED") { - await sleep(e.data.retry_after_ms); - return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry - } - - throw e; - }); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); - break; - } - } - setBusy(false); - onFinished(); - }; - buttonLabel = busy ? _t("Adding...") : _t("Add"); - } - return

{ _t("What do you want to organise?") }

@@ -558,29 +524,18 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { "no one will be informed. You can add more later.") }
- { error &&
{ error }
} - { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} + emptySelectionButton={ + + { _t("Skip for now") } + + } + onFinished={onFinished} />
- - { buttonLabel } - +
; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 8ac68e99b7..c952614c9f 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext, useMemo, useState} from "react"; +import React, {ReactNode, useContext, useMemo, useState} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -58,14 +58,23 @@ const Entry = ({ room, checked, onChange }) => { interface IAddExistingToSpaceProps { space: Room; - selected: Set; - onChange(checked: boolean, room: Room): void; + footerPrompt?: ReactNode; + emptySelectionButton?: ReactNode; + onFinished(added: boolean): void; } -export const AddExistingToSpace: React.FC = ({ space, selected, onChange }) => { +export const AddExistingToSpace: React.FC = ({ + space, + footerPrompt, + emptySelectionButton, + onFinished, +}) => { const cli = useContext(MatrixClientContext); const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); @@ -93,120 +102,6 @@ export const AddExistingToSpace: React.FC = ({ space, return arr; }, [[], [], []]); - return
- - - { rooms.length > 0 ? ( -
-

{ _t("Rooms") }

- { rooms.map(room => { - return { - onChange(checked, room); - } : null} - />; - }) } -
- ) : undefined } - - { spaces.length > 0 ? ( -
-

{ _t("Spaces") }

-
-
{ _t("Feeling experimental?") }
-
{ _t("You can add existing spaces to a space.") }
-
- { spaces.map(space => { - return { - onChange(checked, space); - } : null} - />; - }) } -
- ) : null } - - { dms.length > 0 ? ( -
-

{ _t("Direct Messages") }

- { dms.map(room => { - return { - onChange(checked, room); - } : null} - />; - }) } -
- ) : null } - - { spaces.length + rooms.length + dms.length < 1 ? - { _t("No results") } - : undefined } -
-
; -}; - -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { - const [selectedSpace, setSelectedSpace] = useState(space); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - - const [progress, setProgress] = useState(null); - const [error, setError] = useState(null); - - let spaceOptionSection; - if (existingSubspaces.length > 0) { - const options = [space, ...existingSubspaces].map((space) => { - const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { - mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, - }); - return
- - { space.name || getDisplayAliasForRoom(space) || space.roomId } -
; - }); - - spaceOptionSection = ( - { - setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space); - }} - value={selectedSpace.roomId} - label={_t("Space selection")} - > - { options } - - ); - } else { - spaceOptionSection =
- { space.name || getDisplayAliasForRoom(space) || space.roomId } -
; - } - - const title = - -
-

{ _t("Add existing rooms") }

- { spaceOptionSection } -
-
; - const addRooms = async () => { setError(null); setProgress(0); @@ -269,20 +164,145 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
; } else { + let button = emptySelectionButton; + if (!button || selectedToAdd.size > 0) { + button = + { _t("Add") } + ; + } + footer = <> -
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(cli, space)} kind="link"> - { _t("Create a new room") } - + { footerPrompt }
- - { _t("Add") } - + { button } ; } + const onChange = !busy && !error ? (checked, room) => { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + } : null; + + return
+ + + { rooms.length > 0 ? ( +
+

{ _t("Rooms") }

+ { rooms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
+ ) : undefined } + + { spaces.length > 0 ? ( +
+

{ _t("Spaces") }

+
+
{ _t("Feeling experimental?") }
+
{ _t("You can add existing spaces to a space.") }
+
+ { spaces.map(space => { + return { + onChange(checked, space); + } : null} + />; + }) } +
+ ) : null } + + { dms.length > 0 ? ( +
+

{ _t("Direct Messages") }

+ { dms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
+ ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
+ +
+ { footer } +
+
; +}; + +const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); + + let spaceOptionSection; + if (existingSubspaces.length > 0) { + const options = [space, ...existingSubspaces].map((space) => { + const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { + mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, + }); + return
+ + { space.name || getDisplayAliasForRoom(space) || space.roomId } +
; + }); + + spaceOptionSection = ( + { + setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space); + }} + value={selectedSpace.roomId} + label={_t("Space selection")} + > + { options } + + ); + } else { + spaceOptionSection =
+ { space.name || getDisplayAliasForRoom(space) || space.roomId } +
; + } + + const title = + +
+

{ _t("Add existing rooms") }

+ { spaceOptionSection } +
+
; + return = ({ matrixClient: cli, space, { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - } : null} + onFinished={onFinished} + footerPrompt={<> +
{ _t("Want to add a new room instead?") }
+ onCreateRoomClick(cli, space)} kind="link"> + { _t("Create a new room") } + + } />
-
- { footer } -
onFinished(false)} />
; }; From 2f28de8472b08ae01440db111b7ddcd937433735 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 13 May 2021 13:22:50 +0100 Subject: [PATCH 13/59] Show alternative button during space creation wizard if user opted to create 0 rooms --- src/components/structures/SpaceRoomView.tsx | 17 ++++++++++------- src/i18n/strings/en_EN.json | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index ed0ae1afe7..583caf4b1e 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -78,6 +78,7 @@ interface IProps { interface IState { phase: Phase; + createdRooms?: boolean; // internal state for the creation wizard showRightPanel: boolean; myMembership: string; } @@ -461,7 +462,8 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { setError(""); setBusy(true); try { - await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => { + const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean); + await Promise.all(filteredRoomNames.map(name => { return createRoom({ createOpts: { preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, @@ -474,7 +476,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { parentSpace: space, }); })); - onFinished(); + onFinished(filteredRoomNames.length > 0); } catch (e) { console.error("Failed to create initial space rooms", e); setError(_t("Failed to create initial space rooms")); @@ -484,7 +486,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let onClick = (ev) => { ev.preventDefault(); - onFinished(); + onFinished(false); }; let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { @@ -585,7 +587,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { ; }; -const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => { +const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => { return

{ _t("Share %(name)s", { name: justCreatedOpts?.createOpts?.name || space.name, @@ -598,7 +600,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
- { _t("Go to my first room") } + { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
@@ -891,13 +893,14 @@ export default class SpaceRoomView extends React.PureComponent { _t("Let's create a room for each of them.") + "\n" + _t("You can add more later too, including already existing ones.") } - onFinished={() => this.setState({ phase: Phase.PublicShare })} + onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })} />; case Phase.PublicShare: return ; case Phase.PrivateScope: @@ -919,7 +922,7 @@ export default class SpaceRoomView extends React.PureComponent { title={_t("What projects are you working on?")} description={_t("We'll create rooms for each of them. " + "You can add more later too, including already existing ones.")} - onFinished={() => this.setState({ phase: Phase.Landing })} + onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })} />; case Phase.PrivateExistingRooms: return Date: Thu, 13 May 2021 14:23:10 +0100 Subject: [PATCH 14/59] Remove redundant `tag` prop --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 2 -- src/components/views/rooms/RoomBreadcrumbs.tsx | 1 - src/components/views/rooms/RoomHeader.js | 1 - src/components/views/rooms/RoomTile.tsx | 1 - 4 files changed, 5 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index e95022687a..f15538eabf 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { User } from "matrix-js-sdk/src/models/user"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { TagID } from '../../../stores/room-list/models'; import RoomAvatar from "./RoomAvatar"; import NotificationBadge from '../rooms/NotificationBadge'; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; @@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; interface IProps { room: Room; avatarSize: number; - tag: TagID; displayBadge?: boolean; forceCount?: boolean; oobData?: object; diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index ea0ff233da..c061b79541 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -98,7 +98,6 @@ export default class RoomBreadcrumbs extends React.PureComponent diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index f856f7f6ef..84248f14c2 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -177,7 +177,6 @@ export default class RoomHeader extends React.Component { roomAvatar = ; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 7303f36489..edd644fd65 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent { const roomAvatar = ; From 6aa477f0f52eeb875c251962f588a83a34f66404 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 13 May 2021 14:23:28 +0100 Subject: [PATCH 15/59] Decorate room avatars with publicity in add existing to space flow --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 7 ++++++- src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 91947be76a..c2960ae62b 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -54,7 +54,8 @@ limitations under the License. display: flex; margin-top: 12px; - .mx_BaseAvatar { + // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_DecoratedRoomAvatar { margin-right: 12px; } @@ -75,6 +76,10 @@ limitations under the License. } .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + .mx_BaseAvatar_image { border-radius: 8px; } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 8ac68e99b7..487eb36713 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -46,7 +47,10 @@ interface IProps extends IDialogProps { const Entry = ({ room, checked, onChange }) => { return

; }; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f0a0605584..f34baf256b 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent { const userId = this.context.getUserId(); let inviteOption; - if (this.props.space.canInvite(userId)) { + if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) { inviteOption = ( Date: Fri, 14 May 2021 10:48:24 +0100 Subject: [PATCH 23/59] remove unused imports and run i18n --- src/components/structures/SpaceRoomView.tsx | 2 -- src/i18n/strings/en_EN.json | 8 +++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 4c4b076fdd..30fe945311 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -52,8 +52,6 @@ import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; -import {sleep} from "../../utils/promise"; -import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index da58680424..aebf690ca7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2040,15 +2040,15 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", + "Not all selected were added": "Not all selected were added", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Filter your rooms and spaces": "Filter your rooms and spaces", "Feeling experimental?": "Feeling experimental?", "You can add existing spaces to a space.": "You can add existing spaces to a space.", "Direct Messages": "Direct Messages", "Space selection": "Space selection", "Add existing rooms": "Add existing rooms", - "Not all selected were added": "Not all selected were added", - "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", - "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Want to add a new room instead?": "Want to add a new room instead?", "Create a new room": "Create a new room", "Matrix ID": "Matrix ID", @@ -2702,8 +2702,6 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "Failed to add rooms to space": "Failed to add rooms to space", - "Adding...": "Adding...", "What do you want to organise?": "What do you want to organise?", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Share %(name)s": "Share %(name)s", From 6f9c2f05a2cd99d49c019b8cb46023c9583a527e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 14 May 2021 10:49:53 +0100 Subject: [PATCH 24/59] remove unused vars/imports --- src/components/views/rooms/RoomBreadcrumbs.tsx | 6 +----- src/components/views/rooms/RoomHeader.js | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index c061b79541..00f13209a2 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { CSSTransition } from "react-transition-group"; -import RoomListStore from "../../../stores/room-list/RoomListStore"; -import { DefaultTagID } from "../../../stores/room-list/models"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Toolbar from "../../../accessibility/Toolbar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { } @@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent public render(): React.ReactElement { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { - const roomTags = RoomListStore.instance.getTagsForRoom(r); - const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( Date: Fri, 14 May 2021 11:22:20 +0100 Subject: [PATCH 25/59] fix roving tab index intercepting home/end in space create menu --- src/accessibility/RovingTabIndex.tsx | 2 +- src/components/structures/ContextMenu.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index b49a90d175..4cb537f318 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn const onKeyDownHandler = useCallback((ev) => { let handled = false; // Don't interfere with input default keydown behaviour - if (handleHomeEnd && ev.target.tagName !== "INPUT") { + if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items switch (ev.key) { case Key.HOME: diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 9d9d57d8a6..ad0f75e162 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent { }; private onKeyDown = (ev: React.KeyboardEvent) => { + // don't let keyboard handling escape the context menu + ev.stopPropagation(); + if (!this.props.managed) { if (ev.key === Key.ESCAPE) { this.props.onFinished(); - ev.stopPropagation(); ev.preventDefault(); } return; @@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent { if (handled) { // consume all other keys in context menu - ev.stopPropagation(); ev.preventDefault(); } }; From 336904cef8544f182aefe73c36d1f35ec3149fe4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 May 2021 11:05:49 -0600 Subject: [PATCH 26/59] Apply suggestions from code review Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- src/utils/MultiLock.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 207035ffce..c50db43d08 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,7 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import SettingsStore from "../../../settings/SettingsStore"; import { VisibilityProvider } from "../filters/VisibilityProvider"; -import {MultiLock} from "../../../utils/MultiLock"; +import { MultiLock } from "../../../utils/MultiLock"; /** * Fired when the Algorithm has determined a list has been updated. diff --git a/src/utils/MultiLock.ts b/src/utils/MultiLock.ts index 97cc30306a..507a924dda 100644 --- a/src/utils/MultiLock.ts +++ b/src/utils/MultiLock.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {EnhancedMap} from "./maps"; +import { EnhancedMap } from "./maps"; import AwaitLock from "await-lock"; export type DoneFn = () => void; From 5093657c717740152948fb2f7ef77e0b35e9452c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 May 2021 14:17:10 -0600 Subject: [PATCH 27/59] Remove "read" level language for new API scope --- src/i18n/strings/en_EN.json | 9 --------- src/widgets/CapabilityText.tsx | 10 ---------- 2 files changed, 19 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5126065031..9d5e17ba2d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -590,33 +590,24 @@ "Change which room, message, or user you're viewing": "Change which room, message, or user you're viewing", "Change the topic of this room": "Change the topic of this room", "See when the topic changes in this room": "See when the topic changes in this room", - "See the current topic for this room": "See the current topic for this room", "Change the topic of your active room": "Change the topic of your active room", "See when the topic changes in your active room": "See when the topic changes in your active room", - "See the current topic in your active room": "See the current topic in your active room", "Change the name of this room": "Change the name of this room", "See when the name changes in this room": "See when the name changes in this room", - "See the current name for this room": "See the current name for this room", "Change the name of your active room": "Change the name of your active room", "See when the name changes in your active room": "See when the name changes in your active room", - "See the current name of your active room": "See the current name of your active room", "Change the avatar of this room": "Change the avatar of this room", "See when the avatar changes in this room": "See when the avatar changes in this room", - "See the current avatar for this room": "See the current avatar for this room", "Change the avatar of your active room": "Change the avatar of your active room", "See when the avatar changes in your active room": "See when the avatar changes in your active room", - "See the current avatar for your active room": "See the current avatar for your active room", "Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave", "See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room", - "See the membership status of anyone in this room": "See the membership status of anyone in this room", "Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave", "See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room", "Send stickers to this room as you": "Send stickers to this room as you", "See when a sticker is posted in this room": "See when a sticker is posted in this room", - "See recent stickers posted to this room": "See recent stickers posted to this room", "Send stickers to your active room as you": "Send stickers to your active room as you", "See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room", - "See recent stickers posted to your active room": "See recent stickers posted to your active room", "with an empty state key": "with an empty state key", "with state key %(stateKey)s": "with state key %(stateKey)s", "Send %(eventType)s events as you in this room": "Send %(eventType)s events as you in this room", diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index f7ff94947d..05e6c59083 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -70,48 +70,40 @@ export class CapabilityText { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the topic of this room"), [EventDirection.Receive]: _td("See when the topic changes in this room"), - [EventDirection.Read]: _td("See the current topic for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the topic of your active room"), [EventDirection.Receive]: _td("See when the topic changes in your active room"), - [EventDirection.Read]: _td("See the current topic in your active room"), }, }, [EventType.RoomName]: { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the name of this room"), [EventDirection.Receive]: _td("See when the name changes in this room"), - [EventDirection.Read]: _td("See the current name for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the name of your active room"), [EventDirection.Receive]: _td("See when the name changes in your active room"), - [EventDirection.Read]: _td("See the current name of your active room"), }, }, [EventType.RoomAvatar]: { [WidgetKind.Room]: { [EventDirection.Send]: _td("Change the avatar of this room"), [EventDirection.Receive]: _td("See when the avatar changes in this room"), - [EventDirection.Read]: _td("See the current avatar for this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Change the avatar of your active room"), [EventDirection.Receive]: _td("See when the avatar changes in your active room"), - [EventDirection.Read]: _td("See the current avatar for your active room"), }, }, [EventType.RoomMember]: { [WidgetKind.Room]: { [EventDirection.Send]: _td("Kick, ban, or invite people to this room, and make you leave"), [EventDirection.Receive]: _td("See when people join, leave, or are invited to this room"), - [EventDirection.Read]: _td("See the membership status of anyone in this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Kick, ban, or invite people to your active room, and make you leave"), [EventDirection.Receive]: _td("See when people join, leave, or are invited to your active room"), - [EventDirection.Read]: _td("See the membership status of anyone in this room"), }, }, }; @@ -121,12 +113,10 @@ export class CapabilityText { [WidgetKind.Room]: { [EventDirection.Send]: _td("Send stickers to this room as you"), [EventDirection.Receive]: _td("See when a sticker is posted in this room"), - [EventDirection.Read]: _td("See recent stickers posted to this room"), }, [GENERIC_WIDGET_KIND]: { [EventDirection.Send]: _td("Send stickers to your active room as you"), [EventDirection.Receive]: _td("See when anyone posts a sticker to your active room"), - [EventDirection.Read]: _td("See recent stickers posted to your active room"), }, }, }; From 56097e55c4d509403fc3d8ccc7d840fdfd8dbd80 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 May 2021 14:22:31 -0600 Subject: [PATCH 28/59] We're ELEMENT --- src/identifiers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/identifiers.ts b/src/identifiers.ts index ebd8b87c99..cc8b2fee4d 100644 --- a/src/identifiers.ts +++ b/src/identifiers.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export const ELEMENT_CLIENT_ID = "im.vector.web"; // root namespace for a lot of our settings +export const ELEMENT_CLIENT_ID = "io.element.web"; From a48d786547cdaa4cab498bbe3ee9bf1d15468e67 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 14 May 2021 22:59:03 +0100 Subject: [PATCH 29/59] Update space order field validity requirements to match msc update --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index b1b8199f93..ba2b91aa2c 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -62,7 +62,7 @@ export const getOrder = (order: string, creationTs: number, roomId: string): Arr if (typeof order === "string" && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); - return charCode >= 0x20 && charCode <= 0x7F; + return charCode >= 0x20 && charCode <= 0x7E; })) { validatedOrder = order; } From fef081c736f80b9193bdf36f30e060dbe55142e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 15 May 2021 15:07:15 +0200 Subject: [PATCH 30/59] Add id for homeserver field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/dialogs/ServerPickerDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 62a2b95c68..1234d43582 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -217,6 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent

From 37348375a2bfca59205546a942eaf18b43f9161f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 15 May 2021 15:07:35 +0200 Subject: [PATCH 31/59] Add id for passpharase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/dialogs/security/AccessSecretStorageDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index ffe513581b..b9428289f4 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -345,6 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent Date: Sat, 15 May 2021 16:13:02 +0200 Subject: [PATCH 32/59] Add mx to passPhraseInput --- .../views/dialogs/security/AccessSecretStorageDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index b9428289f4..e09b39f4c7 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -345,7 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent Date: Sat, 15 May 2021 16:13:26 +0200 Subject: [PATCH 33/59] Add mx to homeserverInput --- src/components/views/dialogs/ServerPickerDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 1234d43582..11fef9e75d 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -217,7 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent

From f0a0d7f9981a29832a9645fc5d93e22a39cb2472 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 May 2021 10:01:24 +0100 Subject: [PATCH 34/59] Fix add reactions prompt button jumping timeline if font set lower than default --- res/css/views/messages/_ReactionsRow.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 244439bf74..e05065eb02 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -20,7 +20,8 @@ limitations under the License. .mx_ReactionsRow_addReactionButton { position: relative; - display: none; // show on hover of the .mx_EventTile + display: inline-block; + visibility: hidden; // show on hover of the .mx_EventTile width: 24px; height: 24px; vertical-align: middle; @@ -39,7 +40,7 @@ limitations under the License. } &.mx_ReactionsRow_addReactionButton_active { - display: inline-block; // keep showing whilst the context menu is shown + visibility: visible; // keep showing whilst the context menu is shown } &:hover, &.mx_ReactionsRow_addReactionButton_active { @@ -51,7 +52,7 @@ limitations under the License. } .mx_EventTile:hover .mx_ReactionsRow_addReactionButton { - display: inline-block; + visibility: visible; } .mx_ReactionsRow_showAll { From a22a1918e102f5506b5850f406b3d2d0d4ee3e92 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 May 2021 10:01:43 +0100 Subject: [PATCH 35/59] Fix add reactions prompt button showing up even if all reactions have been removed --- src/components/views/messages/ReactionsRow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index e1fcbe5364..627f7b35a5 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -174,6 +174,8 @@ export default class ReactionsRow extends React.PureComponent { />; }).filter(item => !!item); + if (!items.length) return null; + // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // The "+ 1" ensure that the "show all" reveals something that takes up // more space than the button itself. From 6e5847ea6b253715c6c3e5a17db67ca46c4dec6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 May 2021 10:01:56 +0100 Subject: [PATCH 36/59] Fix right clicking on add reactions prompt button behaviour --- src/components/views/messages/ReactionsRow.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 627f7b35a5..8809302088 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -50,6 +50,10 @@ const ReactButton = ({ mxEvent, reactions }: IProps) => { })} title={_t("Add reaction")} onClick={openMenu} + onContextMenu={e => { + e.preventDefault(); + openMenu(); + }} isExpanded={menuDisplayed} inputRef={button} /> From 6c066ee40148098dc254e39b2920b00b330df0c2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 17 May 2021 13:33:45 +0100 Subject: [PATCH 37/59] Upgrade matrix-js-sdk to 11.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a7fd78b903..160867776f 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.0.0-rc.1", + "matrix-js-sdk": "11.0.0", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 4fe12f3768..be735eea39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,10 +5669,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@11.0.0-rc.1: - version "11.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.0.0-rc.1.tgz#a40fdc9e93a1a9e873dd867e5766a20ea780e1d8" - integrity sha512-zATTKWH4yrs6mKcLQNvhvwi1IRGUEx3xt3MPCFuSuQq23TMbnxhOZMMpUF6n19FpioHw9jbz7cPsdVKM2gpDLw== +matrix-js-sdk@11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.0.0.tgz#66428b3d7606acd0d566ebc7cc7333e15f25b2a8" + integrity sha512-54yhqGRlogNv1QKpnn5kDAJ6z5MwoXH/Yqv0cFpq0lS1mzVJUIg4urpNPQBDidcA0IqGhu4aYUuy5s1cwHyTsg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 6b884a50dc6ad0aef07976fc15af85940f9e4e7d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 17 May 2021 13:48:59 +0100 Subject: [PATCH 38/59] Prepare changelog for v3.21.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe714b2b89..2582668ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0) + +## Security notice + +matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv) +related to file upload. When uploading a file, the local file preview can lead +to execution of scripts embedded in the uploaded file, but only after several +user interactions to open the preview in a separate tab. This only impacts the +local user while in the process of uploading. It cannot be exploited remotely +or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV) +for responsibly disclosing this via Matrix's Security Disclosure Policy. + +## All changes + + * Upgrade to JS SDK 11.0.0 + * [Release] Add missing space on beta feedback dialog + [\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019) + * [Release] Add feedback mechanism for beta features, namely Spaces + [\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013) + * Add feedback mechanism for beta features, namely Spaces + [\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012) + Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1) From 3673292c3495a40df781edd3e7f4c510b39875dc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 17 May 2021 13:49:00 +0100 Subject: [PATCH 39/59] v3.21.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 160867776f..241bc45512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.21.0-rc.1", + "version": "3.21.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 9d46fa7b994da54f8e48a70f6c0f46074ec6f319 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 17 May 2021 13:50:24 +0100 Subject: [PATCH 40/59] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 176a0a0f3f..4e6b3e074a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -200,6 +200,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 12a9ce1a94440e6b96f739fb5e4f148a8f100535 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 17 May 2021 13:50:34 +0100 Subject: [PATCH 41/59] 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 4e6b3e074a..1bd381d26c 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index be735eea39..1a3b746727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,10 +5669,9 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@11.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "11.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.0.0.tgz#66428b3d7606acd0d566ebc7cc7333e15f25b2a8" - integrity sha512-54yhqGRlogNv1QKpnn5kDAJ6z5MwoXH/Yqv0cFpq0lS1mzVJUIg4urpNPQBDidcA0IqGhu4aYUuy5s1cwHyTsg== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/52a893a8116d60bb76f1b015d3161a15404b3628" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From c67f31144f279d6e87169376b1646c8ca53ff38e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 17 May 2021 14:45:41 +0100 Subject: [PATCH 42/59] remove custom LoggedInView::shouldComponentUpdate logic --- src/components/structures/LoggedInView.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index c4b9696807..eb5d49f56d 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -219,16 +219,6 @@ class LoggedInView extends React.Component { }); }; - // Child components assume that the client peg will not be null, so give them some - // sort of assurance here by only allowing a re-render if the client is truthy. - // - // This is required because `LoggedInView` maintains its own state and if this state - // updates after the client peg has been made null (during logout), then it will - // attempt to re-render and the children will throw errors. - shouldComponentUpdate() { - return Boolean(MatrixClientPeg.get()); - } - canResetTimelineInRoom = (roomId) => { if (!this._roomView.current) { return true; From 7e846b8532453e7e74499a9790fff55fe91f51d8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 17 May 2021 15:05:20 +0100 Subject: [PATCH 43/59] remove unused import --- 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 eb5d49f56d..ad5c759f0d 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; -import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; +import { IMatrixClientCreds } from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import TagOrderActions from '../../actions/TagOrderActions'; From d3f1754dfd9fceff5df45b4f8cdaf9e606214dbc Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 17 May 2021 12:24:41 -0400 Subject: [PATCH 44/59] Fix crash on opening notification panel The check for pending edits needed a null guard, since the notification panel uses MessagePanel but is not associated with a specific room. Signed-off-by: Robin Townsend --- 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 73a2a3c4b6..d1071a9e19 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -473,7 +473,7 @@ export default class MessagePanel extends React.Component { } get _roomHasPendingEdit() { - return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); + return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); } _getEventTiles() { From 7cdca77f3902ac4d708936173990b6b23347add6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 May 2021 16:27:48 -0600 Subject: [PATCH 45/59] Update widget-api --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1bd381d26c..f8d94e2d21 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.13", + "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 1a3b746727..19c0646d32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5704,10 +5704,10 @@ matrix-react-test-utils@^0.2.2: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.13: - version "0.1.0-beta.13" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef" - integrity sha512-DJAvuX2E7gxc/a9rtJPDh17ba9xGIOAoBHcWirNTN3KGodzsrZ+Ns+M/BREFWMwGS5yEBZko5eq7uhXStEbnyQ== +matrix-widget-api@^0.1.0-beta.14: + version "0.1.0-beta.14" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" + integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From 3a2c1af70c48acf6eff5eff11ee417c52bc681d9 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 18 May 2021 00:01:08 -0500 Subject: [PATCH 46/59] Don't mark a room as unread when redacted event is present Signed-off-by: Aaron Raimist --- src/Unread.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Unread.js b/src/Unread.js index 12c15eb6af..25c425aa9a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -40,6 +40,8 @@ export function eventTriggersUnreadCount(ev) { return false; } else if (ev.getType() == 'm.room.server_acl') { return false; + } else if (ev.isRedacted()) { + return false; } return haveTileForEvent(ev); } From 19a94d55bd4883545869f2ef299853326bf79ba5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 09:39:01 +0100 Subject: [PATCH 47/59] Create SpaceProvider and hide Spaces from the RoomProvider autocompleter --- src/autocomplete/Autocompleter.ts | 7 +++++ src/autocomplete/RoomProvider.tsx | 31 +++++++++++++-------- src/autocomplete/SpaceProvider.tsx | 43 ++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/autocomplete/SpaceProvider.tsx diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 8618fc3a77..5409825f45 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import {timeout} from "../utils/promise"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; +import SettingsStore from "../settings/SettingsStore"; +import SpaceProvider from "./SpaceProvider"; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -56,6 +58,11 @@ const PROVIDERS = [ DuckDuckGoProvider, ]; +// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here +if (SettingsStore.getValue("feature_spaces")) { + PROVIDERS.push(SpaceProvider); +} + // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 249c069080..ad55b19101 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -1,8 +1,7 @@ /* Copyright 2016 Aviral Dasgupta -Copyright 2017 Vector Creations Ltd -Copyright 2017, 2018 New Vector Ltd Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2017, 2018, 2021 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. @@ -17,17 +16,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import {uniqBy, sortBy} from "lodash"; import Room from "matrix-js-sdk/src/models/room"; + import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; -import * as sdk from '../index'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; -import {uniqBy, sortBy} from "lodash"; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import SettingsStore from "../settings/SettingsStore"; const ROOM_REGEX = /\B#\S*/g; @@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") { } export default class RoomProvider extends AutocompleteProvider { - matcher: QueryMatcher; + protected matcher: QueryMatcher; constructor() { super(ROOM_REGEX); @@ -58,20 +59,28 @@ export default class RoomProvider extends AutocompleteProvider { }); } + protected getRooms() { + const cli = MatrixClientPeg.get(); + let rooms = cli.getVisibleRooms(); + + if (SettingsStore.getValue("feature_spaces")) { + rooms = rooms.filter(r => !r.isSpaceRoom()); + } + + return rooms; + } + async getCompletions( query: string, selection: ISelectionRange, force = false, limit = -1, ): Promise { - const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); - - const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => { + let matcherObjects = this.getRooms().reduce((aliases, room) => { if (room.getCanonicalAlias()) { aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); } @@ -115,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); + }).filter((completion) => !!completion.completion && completion.completion.length > 0); } return completions; } diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx new file mode 100644 index 0000000000..0361a2c91e --- /dev/null +++ b/src/autocomplete/SpaceProvider.tsx @@ -0,0 +1,43 @@ +/* +Copyright 2021 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 { _t } from '../languageHandler'; +import {MatrixClientPeg} from '../MatrixClientPeg'; +import RoomProvider from "./RoomProvider"; + +export default class SpaceProvider extends RoomProvider { + protected getRooms() { + return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom()); + } + + getName() { + return _t("Spaces"); + } + + renderCompletions(completions: React.ReactNode[]): React.ReactNode { + return ( +

+ { completions } +
+ ); + } +} From f87f0704b980c491390de8ed25e8903fd2cb08ac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 09:42:35 +0100 Subject: [PATCH 48/59] i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9d5e17ba2d..74ebc8304a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2837,6 +2837,7 @@ "Room Notification": "Room Notification", "Notification Autocomplete": "Notification Autocomplete", "Room Autocomplete": "Room Autocomplete", + "Space Autocomplete": "Space Autocomplete", "Users": "Users", "User Autocomplete": "User Autocomplete", "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.", From 871c48f69b8039f82da7aeaff0b8a6e87ae973a5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 10:02:21 +0100 Subject: [PATCH 49/59] stop assuming that decryption happens ahead of time --- src/components/structures/FilePanel.js | 4 ++++ src/components/structures/RoomView.tsx | 2 +- src/components/views/messages/MessageActionBar.js | 7 +++++++ src/components/views/messages/ViewSourceEvent.js | 7 +++++++ src/indexing/EventIndex.js | 4 ++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index d5e4b092e2..8f9908f1ca 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -50,6 +50,10 @@ class FilePanel extends React.Component { if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; + if (ev.shouldAttemptDecryption()) { + ev.attemptDecryption(room._client._crypto); + } + if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); } else { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0f3c59457..dbfba13297 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -811,7 +811,7 @@ export default class RoomView extends React.Component { }; private onEvent = (ev) => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; + if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return; this.handleEffects(ev); }; diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index b2f7f8a692..cf3a0a704f 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -31,6 +31,7 @@ import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessi import {replaceableComponent} from "../../../utils/replaceableComponent"; import {canCancel} from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -122,6 +123,12 @@ export default class MessageActionBar extends React.PureComponent { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { this.props.mxEvent.on("Event.status", this.onSent); } + + if (this.props.mxEvent.shouldAttemptDecryption()) { + const client = MatrixClientPeg.get(); + this.props.mxEvent.attemptDecryption(client._crypto); + } + if (this.props.mxEvent.isBeingDecrypted()) { this.props.mxEvent.once("Event.decrypted", this.onDecrypted); } diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index adc7a248cd..9a110a8826 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -18,6 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; @replaceableComponent("views.messages.ViewSourceEvent") export default class ViewSourceEvent extends React.PureComponent { @@ -36,6 +37,12 @@ export default class ViewSourceEvent extends React.PureComponent { componentDidMount() { const {mxEvent} = this.props; + + const client = MatrixClientPeg.get(); + if (mxEvent.shouldAttemptDecryption()) { + mxEvent.attemptDecryption(client._client._crypto); + } + if (mxEvent.isBeingDecrypted()) { mxEvent.once("Event.decrypted", () => this.forceUpdate()); } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 857dc5b248..df46d800b1 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -187,6 +187,10 @@ export default class EventIndex extends EventEmitter { return; } + if (ev.shouldAttemptDecryption()) { + ev.attemptDecryption(room._client._crypto); + } + if (ev.isBeingDecrypted()) { // XXX: Private member access await ev._decryptionPromise; From 1166e7692845716740ca1f281d825274664a588c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 10:28:15 +0100 Subject: [PATCH 50/59] Extract and deduplicate chat effect interfaces --- src/effects/effect.ts | 43 +++++++++++++++++++++++ src/effects/index.ts | 79 +++---------------------------------------- 2 files changed, 48 insertions(+), 74 deletions(-) create mode 100644 src/effects/effect.ts diff --git a/src/effects/effect.ts b/src/effects/effect.ts new file mode 100644 index 0000000000..9011b07b61 --- /dev/null +++ b/src/effects/effect.ts @@ -0,0 +1,43 @@ +/* + Copyright 2020 Nurjin Jafar + Copyright 2020 Nordeck IT + Consulting GmbH. + + 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 type Effect = { + /** + * one or more emojis that will trigger this effect + */ + emojis: Array; + /** + * the matrix message type that will trigger this effect + */ + msgType: string; + /** + * the room command to trigger this effect + */ + command: string; + /** + * a function that returns the translated description of the effect + */ + description: () => string; + /** + * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message + */ + fallbackMessage: () => string; + /** + * animation options + */ + options: TOptions; +} diff --git a/src/effects/index.ts b/src/effects/index.ts index a22948ebcf..1a6858be08 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -15,80 +15,11 @@ limitations under the License. */ import { _t, _td } from "../languageHandler"; - -export type Effect = { - /** - * one or more emojis that will trigger this effect - */ - emojis: Array; - /** - * the matrix message type that will trigger this effect - */ - msgType: string; - /** - * the room command to trigger this effect - */ - command: string; - /** - * a function that returns the translated description of the effect - */ - description: () => string; - /** - * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message - */ - fallbackMessage: () => string; - /** - * animation options - */ - options: TOptions; -} - -type ConfettiOptions = { - /** - * max confetti count - */ - maxCount: number; - /** - * particle animation speed - */ - speed: number; - /** - * the confetti animation frame interval in milliseconds - */ - frameInterval: number; - /** - * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible) - */ - alpha: number; - /** - * use gradient instead of solid particle color - */ - gradient: boolean; -}; -type FireworksOptions = { - /** - * max fireworks count - */ - maxCount: number; - /** - * gravity value that firework adds to shift from it's start position - */ - gravity: number; -} -type SnowfallOptions = { - /** - * The maximum number of snowflakes to render at a given time - */ - maxCount: number; - /** - * The amount of gravity to apply to the snowflakes - */ - gravity: number; - /** - * The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies. - */ - maxDrift: number; -} +import { ConfettiOptions } from "./confetti"; +import { Effect } from "./effect"; +import { FireworksOptions } from "./fireworks"; +import { SnowfallOptions } from "./snowfall"; +import { SpaceInvadersOptions } from "./spaceinvaders"; /** * This configuration defines room effects that can be triggered by custom message types and emojis From afd984372200c75a9a5787db4dc706ed18c2582a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 10:28:37 +0100 Subject: [PATCH 51/59] Fix broken string interpolation in chat effects warning --- src/components/views/elements/EffectsOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 38be8da9a8..7bed0222b0 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -37,7 +37,7 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { effect = new Effect(options); effectsRef.current[name] = effect; } catch (err) { - console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err); + console.warn(`Unable to load effect module at '../../../effects/${name}.`, err); } } return effect; From b5a612cd0fb0a50ecf6f831cffc0a8e0678d03ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 10:29:00 +0100 Subject: [PATCH 52/59] Add space invaders chat effect --- src/effects/index.ts | 11 +++ src/effects/spaceinvaders/index.ts | 119 +++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + 3 files changed, 132 insertions(+) create mode 100644 src/effects/spaceinvaders/index.ts diff --git a/src/effects/index.ts b/src/effects/index.ts index 1a6858be08..8ecb80020d 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -62,6 +62,17 @@ export const CHAT_EFFECTS: Array> = [ maxDrift: 5, }, } as Effect, + { + emojis: ["👾", "🌌"], + msgType: "io.element.effects.space_invaders", + command: "spaceinvaders", + description: () => _td("Sends the given message with a space themed effect"), + fallbackMessage: () => _t("sends space invaders") + " 👾", + options: { + maxCount: 50, + gravity: 0.01, + }, + } as Effect, ]; diff --git a/src/effects/spaceinvaders/index.ts b/src/effects/spaceinvaders/index.ts new file mode 100644 index 0000000000..55eb6dc29f --- /dev/null +++ b/src/effects/spaceinvaders/index.ts @@ -0,0 +1,119 @@ +/* + Copyright 2021 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 ICanvasEffect from '../ICanvasEffect'; +import { arrayFastClone } from "../../utils/arrays"; + +export type SpaceInvadersOptions = { + /** + * The maximum number of invaders to render at a given time + */ + maxCount: number; + /** + * The amount of gravity to apply to the invaders + */ + gravity: number; +} + +type Invader = { + x: number; + y: number; + xCol: number; + gravity: number; +} + +export const DefaultOptions: SpaceInvadersOptions = { + maxCount: 50, + gravity: 0.005, +}; + +const KEY_FRAME_INTERVAL = 15; // 15ms, roughly +const GLYPH = "👾"; + +export default class SpaceInvaders implements ICanvasEffect { + private readonly options: SpaceInvadersOptions; + + constructor(options: { [key: string]: any }) { + this.options = {...DefaultOptions, ...options}; + } + + private context: CanvasRenderingContext2D | null = null; + private particles: Array = []; + private lastAnimationTime: number; + + public isRunning: boolean; + + public start = async (canvas: HTMLCanvasElement, timeout = 3000) => { + if (!canvas) { + return; + } + this.context = canvas.getContext('2d'); + this.particles = []; + const count = this.options.maxCount; + while (this.particles.length < count) { + this.particles.push(this.resetParticle({} as Invader, canvas.width, canvas.height)); + } + this.isRunning = true; + requestAnimationFrame(this.renderLoop); + if (timeout) { + window.setTimeout(this.stop, timeout); + } + } + + public stop = async () => { + this.isRunning = false; + } + + private resetParticle = (particle: Invader, width: number, height: number): Invader => { + particle.x = Math.random() * width; + particle.y = Math.random() * -height; + particle.xCol = particle.x; + particle.gravity = this.options.gravity + (Math.random() * 6) + 4; + return particle; + } + + private renderLoop = (): void => { + if (!this.context || !this.context.canvas) { + return; + } + if (this.particles.length === 0) { + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + } else { + const timeDelta = Date.now() - this.lastAnimationTime; + if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) { + // Clear the screen first + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + + this.lastAnimationTime = Date.now(); + this.animateAndRenderSnowflakes(); + } + requestAnimationFrame(this.renderLoop); + } + }; + + private animateAndRenderSnowflakes() { + if (!this.context || !this.context.canvas) { + return; + } + this.context.font = "50px Twemoji"; + for (const particle of arrayFastClone(this.particles)) { + particle.y += particle.gravity; + + this.context.save(); + this.context.fillText(GLYPH, particle.x, particle.y); + this.context.restore(); + } + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9d5e17ba2d..5e0cd4c47c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -884,6 +884,8 @@ "sends fireworks": "sends fireworks", "Sends the given message with snowfall": "Sends the given message with snowfall", "sends snowfall": "sends snowfall", + "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", + "sends space invaders": "sends space invaders", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", From 1cfd4b6e1aa176f10dfb572cbdb7cad92a7f467a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 11:41:20 +0100 Subject: [PATCH 53/59] Use client.decryptEvent to avoid accessing js-sdk private members --- src/components/structures/FilePanel.js | 5 ++-- src/components/structures/TimelinePanel.js | 3 +- .../views/messages/MessageActionBar.js | 2 +- .../views/messages/ViewSourceEvent.js | 2 +- src/indexing/EventIndex.js | 30 +++++-------------- 5 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 8f9908f1ca..ff30c25073 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -50,9 +50,8 @@ class FilePanel extends React.Component { if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; - if (ev.shouldAttemptDecryption()) { - ev.attemptDecryption(room._client._crypto); - } + const client = MatrixClientPeg.get(); + client.decryptEvent(ev); if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5012d91a5f..4918a4a3b4 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1150,7 +1150,8 @@ class TimelinePanel extends React.Component { .reverse() .forEach(event => { if (event.shouldAttemptDecryption()) { - event.attemptDecryption(MatrixClientPeg.get()._crypto); + const client = MatrixClientPeg.get(); + client.decryptEvent(event); } }); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index cf3a0a704f..21b7cd64c9 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -126,7 +126,7 @@ export default class MessageActionBar extends React.PureComponent { if (this.props.mxEvent.shouldAttemptDecryption()) { const client = MatrixClientPeg.get(); - this.props.mxEvent.attemptDecryption(client._crypto); + client.decryptEvent(this.props.mxEvent); } if (this.props.mxEvent.isBeingDecrypted()) { diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index 9a110a8826..9e30261fd6 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -40,7 +40,7 @@ export default class ViewSourceEvent extends React.PureComponent { const client = MatrixClientPeg.get(); if (mxEvent.shouldAttemptDecryption()) { - mxEvent.attemptDecryption(client._client._crypto); + client.decryptEvent(mxEvent); } if (mxEvent.isBeingDecrypted()) { diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index df46d800b1..4d207d9f49 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -177,8 +177,10 @@ export default class EventIndex extends EventEmitter { * listener. */ onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + const client = MatrixClientPeg.get(); + // We only index encrypted rooms locally. - if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + if (!client.isRoomEncrypted(room.roomId)) return; // If it isn't a live event or if it's redacted there's nothing to // do. @@ -187,14 +189,7 @@ export default class EventIndex extends EventEmitter { return; } - if (ev.shouldAttemptDecryption()) { - ev.attemptDecryption(room._client._crypto); - } - - if (ev.isBeingDecrypted()) { - // XXX: Private member access - await ev._decryptionPromise; - } + await client.decryptEvent(ev); await this.addLiveEventToIndex(ev); } @@ -522,19 +517,10 @@ export default class EventIndex extends EventEmitter { const decryptionPromises = matrixEvents .filter(event => event.isEncrypted()) .map(event => { - if (event.shouldAttemptDecryption()) { - return event.attemptDecryption(client._crypto, { - isRetry: true, - emit: false, - }); - } else { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - return event._decryptionPromise; - } + return client.decryptEvent(event, { + isRetry: true, + emit: false, + }); }); // Let us wait for all the events to get decrypted. From 177adb9684f7bd15aa59aa3a558204e4773cb881 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 11:58:15 +0100 Subject: [PATCH 54/59] fix copy-pasta --- src/effects/spaceinvaders/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/effects/spaceinvaders/index.ts b/src/effects/spaceinvaders/index.ts index 55eb6dc29f..9520e8366a 100644 --- a/src/effects/spaceinvaders/index.ts +++ b/src/effects/spaceinvaders/index.ts @@ -97,13 +97,13 @@ export default class SpaceInvaders implements ICanvasEffect { this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); this.lastAnimationTime = Date.now(); - this.animateAndRenderSnowflakes(); + this.animateAndRenderInvaders(); } requestAnimationFrame(this.renderLoop); } }; - private animateAndRenderSnowflakes() { + private animateAndRenderInvaders() { if (!this.context || !this.context.canvas) { return; } From f9f10de0da767431a8bead387b4b960993537f4a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 13:01:38 +0100 Subject: [PATCH 55/59] use renamed decrypt event method --- src/components/structures/FilePanel.js | 2 +- src/components/structures/TimelinePanel.js | 6 ++---- src/components/views/messages/MessageActionBar.js | 6 ++---- src/components/views/messages/ViewSourceEvent.js | 4 +--- src/indexing/EventIndex.js | 4 ++-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index ff30c25073..bb7c1f9642 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -51,7 +51,7 @@ class FilePanel extends React.Component { if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; const client = MatrixClientPeg.get(); - client.decryptEvent(ev); + client.decryptEventIfNeeded(ev); if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4918a4a3b4..af20c31cb2 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1149,10 +1149,8 @@ class TimelinePanel extends React.Component { arrayFastClone(events) .reverse() .forEach(event => { - if (event.shouldAttemptDecryption()) { - const client = MatrixClientPeg.get(); - client.decryptEvent(event); - } + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(event); }); const firstVisibleEventIndex = this._checkForPreJoinUISI(events); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 21b7cd64c9..37737519ce 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -124,10 +124,8 @@ export default class MessageActionBar extends React.PureComponent { this.props.mxEvent.on("Event.status", this.onSent); } - if (this.props.mxEvent.shouldAttemptDecryption()) { - const client = MatrixClientPeg.get(); - client.decryptEvent(this.props.mxEvent); - } + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(this.props.mxEvent); if (this.props.mxEvent.isBeingDecrypted()) { this.props.mxEvent.once("Event.decrypted", this.onDecrypted); diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index 9e30261fd6..2ec567c5ad 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -39,9 +39,7 @@ export default class ViewSourceEvent extends React.PureComponent { const {mxEvent} = this.props; const client = MatrixClientPeg.get(); - if (mxEvent.shouldAttemptDecryption()) { - client.decryptEvent(mxEvent); - } + client.decryptEventIfNeeded(mxEvent); if (mxEvent.isBeingDecrypted()) { mxEvent.once("Event.decrypted", () => this.forceUpdate()); diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 4d207d9f49..ed4418140b 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -189,7 +189,7 @@ export default class EventIndex extends EventEmitter { return; } - await client.decryptEvent(ev); + await client.decryptEventIfNeeded(ev); await this.addLiveEventToIndex(ev); } @@ -517,7 +517,7 @@ export default class EventIndex extends EventEmitter { const decryptionPromises = matrixEvents .filter(event => event.isEncrypted()) .map(event => { - return client.decryptEvent(event, { + return client.decryptEventIfNeeded(event, { isRetry: true, emit: false, }); From 454df8947be27c9fe1529043061b07e007a000c6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 13:46:47 +0100 Subject: [PATCH 56/59] Add mock for new client method --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 6dc02463a5..953693a820 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -95,6 +95,7 @@ export function createTestClient() { getItem: jest.fn(), }, }, + decryptEventIfNeeded: () => Promise.resolve(), }; } From 0e221ae5488c99935f09a051f3da121ac5899e47 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 16:24:38 +0100 Subject: [PATCH 57/59] Start decryption process if needed --- src/Notifier.ts | 2 ++ src/stores/widgets/StopGapWidget.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Notifier.ts b/src/Notifier.ts index 3e927cea0c..4f55046e72 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -331,6 +331,8 @@ export const Notifier = { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; + MatrixClientPeg.get().decryptEventIfNeeded(ev); + // If it's an encrypted event and the type is still 'm.room.encrypted', // it hasn't yet been decrypted, so wait until it is. if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 84cf35aeb4..397d637125 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -400,6 +400,7 @@ export class StopGapWidget extends EventEmitter { } private onEvent = (ev: MatrixEvent) => { + MatrixClientPeg.get().decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); From fc6ff861730ae821b7cd5dda288a5c42a286a3a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 May 2021 16:34:00 +0100 Subject: [PATCH 58/59] Add error detail when languges fail to load This will at least log the path that's failing with status code, so we can better confirm the issue. Related to https://github.com/vector-im/element-web/issues/9422 --- src/languageHandler.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index f30e735f03..4a3fb30886 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -464,10 +464,14 @@ function getLangsJson(): Promise { request( { method: "GET", url }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${url}, got ${response.status}`)); + return; + } resolve(JSON.parse(body)); }, ); @@ -507,10 +511,14 @@ function getLanguage(langPath: string): Promise { request( { method: "GET", url: langPath }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${langPath}, got ${response.status}`)); + return; + } resolve(weblateToCounterpart(JSON.parse(body))); }, ); From f7d0afcd287d79a8fc673197facb055311c3f840 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 19 May 2021 10:07:02 +0100 Subject: [PATCH 59/59] Performance monitoring measurements (#6041) --- src/@types/global.d.ts | 3 + src/components/structures/MatrixChat.tsx | 46 ++---- src/performance/entry-names.ts | 57 ++++++++ src/performance/index.ts | 178 +++++++++++++++++++++++ test/end-to-end-tests/.gitignore | 1 + test/end-to-end-tests/src/session.js | 2 +- test/end-to-end-tests/start.js | 20 ++- 7 files changed, 275 insertions(+), 32 deletions(-) create mode 100644 src/performance/entry-names.ts create mode 100644 src/performance/index.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f04a2ff237..63966d96fa 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -42,6 +42,7 @@ import {SpaceStoreClass} from "../stores/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; +import PerformanceMonitor from "../performance"; declare global { interface Window { @@ -79,6 +80,8 @@ declare global { mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; + mxPerformanceMonitor: PerformanceMonitor; + mxPerformanceEntryNames: any; } interface Document { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 288acc108a..4c7fca2fec 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -86,6 +86,8 @@ import {RoomUpdateCause} from "../../stores/room-list/models"; import defaultDispatcher from "../../dispatcher/dispatcher"; import SecurityCustomisations from "../../customisations/Security"; +import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; + /** constants for MatrixChat.state.view */ export enum Views { // a special initial state which is only used at startup, while we are @@ -484,42 +486,22 @@ export default class MatrixChat extends React.PureComponent { } startPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; - - // This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate - // are used. - if (this.pageChanging) { - console.warn('MatrixChat.startPageChangeTimer: timer already started'); - return; - } - this.pageChanging = true; - performance.mark('element_MatrixChat_page_change_start'); + PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; + const perfMonitor = PerformanceMonitor.instance; - if (!this.pageChanging) { - console.warn('MatrixChat.stopPageChangeTimer: timer not started'); - return; - } - this.pageChanging = false; - performance.mark('element_MatrixChat_page_change_stop'); - performance.measure( - 'element_MatrixChat_page_change_delta', - 'element_MatrixChat_page_change_start', - 'element_MatrixChat_page_change_stop', - ); - performance.clearMarks('element_MatrixChat_page_change_start'); - performance.clearMarks('element_MatrixChat_page_change_stop'); - const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop(); + perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); - // In practice, sometimes the entries list is empty, so we get no measurement - if (!measurement) return null; + const entries = perfMonitor.getEntries({ + name: PerformanceEntryNames.PAGE_CHANGE, + }); + const measurement = entries.pop(); - return measurement.duration; + return measurement + ? measurement.duration + : null; } shouldTrackPageChange(prevState: IState, state: IState) { @@ -1632,11 +1614,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1965,6 +1949,8 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); + PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); }; // complete security / e2e setup has finished diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts new file mode 100644 index 0000000000..effd9506f6 --- /dev/null +++ b/src/performance/entry-names.ts @@ -0,0 +1,57 @@ +/* +Copyright 2021 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 PerformanceEntryNames { + + /** + * Application wide + */ + + APP_STARTUP = "mx_AppStartup", + PAGE_CHANGE = "mx_PageChange", + + /** + * Events + */ + + RESEND_EVENT = "mx_ResendEvent", + SEND_E2EE_EVENT = "mx_SendE2EEEvent", + SEND_ATTACHMENT = "mx_SendAttachment", + + /** + * Rooms + */ + + SWITCH_ROOM = "mx_SwithRoom", + JUMP_TO_ROOM = "mx_JumpToRoom", + JOIN_ROOM = "mx_JoinRoom", + CREATE_DM = "mx_CreateDM", + PEEK_ROOM = "mx_PeekRoom", + + /** + * User + */ + + VERIFY_E2EE_USER = "mx_VerifyE2EEUser", + LOGIN = "mx_Login", + REGISTER = "mx_Register", + + /** + * VoIP + */ + + SETUP_VOIP_CALL = "mx_SetupVoIPCall", +} diff --git a/src/performance/index.ts b/src/performance/index.ts new file mode 100644 index 0000000000..bfb5b4a9c7 --- /dev/null +++ b/src/performance/index.ts @@ -0,0 +1,178 @@ +/* +Copyright 2021 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 { PerformanceEntryNames } from "./entry-names"; + +interface GetEntriesOptions { + name?: string, + type?: string, +} + +type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; + +interface PerformanceDataListener { + entryNames?: string[], + callback: PerformanceCallbackFunction +} + +export default class PerformanceMonitor { + static _instance: PerformanceMonitor; + + private START_PREFIX = "start:" + private STOP_PREFIX = "stop:" + + private listeners: PerformanceDataListener[] = [] + private entries: PerformanceEntry[] = [] + + public static get instance(): PerformanceMonitor { + if (!PerformanceMonitor._instance) { + PerformanceMonitor._instance = new PerformanceMonitor(); + } + return PerformanceMonitor._instance; + } + + /** + * Starts a performance recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + start(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + + if (performance.getEntriesByName(this.START_PREFIX + key).length > 0) { + console.warn(`Recording already started for: ${name}`); + return; + } + + performance.mark(this.START_PREFIX + key); + } + + /** + * Stops a performance recording and stores delta duration + * with the start marker + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + stop(name: string, id?: string): PerformanceEntry { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + if (performance.getEntriesByName(this.START_PREFIX + key).length === 0) { + console.warn(`No recording started for: ${name}`); + return; + } + + performance.mark(this.STOP_PREFIX + key); + performance.measure( + key, + this.START_PREFIX + key, + this.STOP_PREFIX + key, + ); + + this.clear(name, id); + + const measurement = performance.getEntriesByName(key).pop(); + + // Keeping a reference to all PerformanceEntry created + // by this abstraction for historical events collection + // when adding a data callback + this.entries.push(measurement); + + this.listeners.forEach(listener => { + if (this.shouldEmit(listener, measurement)) { + listener.callback([measurement]) + } + }); + + return measurement; + } + + clear(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + performance.clearMarks(this.START_PREFIX + key); + performance.clearMarks(this.STOP_PREFIX + key); + } + + getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { + return this.entries.filter(entry => { + const satisfiesName = !name || entry.name === name; + const satisfiedType = !type || entry.entryType === type; + return satisfiesName && satisfiedType; + }); + } + + addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { + this.listeners.push(listener); + if (buffer) { + const toEmit = this.entries.filter(entry => this.shouldEmit(listener, entry)); + if (toEmit.length > 0) { + listener.callback(toEmit); + } + } + } + + removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { + if (!callback) { + this.listeners = []; + } else { + this.listeners.splice( + this.listeners.findIndex(listener => listener.callback === callback), + 1, + ); + } + } + + /** + * Tor browser does not support the Performance API + * @returns {boolean} true if the Performance API is supported + */ + private supportsPerformanceApi(): boolean { + return performance !== undefined && performance.mark !== undefined; + } + + private shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean { + return !listener.entryNames || listener.entryNames.includes(entry.name); + } + + /** + * Internal utility to ensure consistent name for the recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {string} a compound of the name and identifier if present + */ + private buildKey(name: string, id?: string): string { + return `${name}${id ? `:${id}` : ''}`; + } +} + + +// Convenience exports +export { + PerformanceEntryNames, +} + +// Exposing those to the window object to bridge them from tests +window.mxPerformanceMonitor = PerformanceMonitor.instance; +window.mxPerformanceEntryNames = PerformanceEntryNames; diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore index 61f9012393..9180d32e90 100644 --- a/test/end-to-end-tests/.gitignore +++ b/test/end-to-end-tests/.gitignore @@ -1,3 +1,4 @@ node_modules *.png element/env +performance-entries.json diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index 4c611ef877..6c68929a0b 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -208,7 +208,7 @@ module.exports = class ElementSession { this.log.done(); } - close() { + async close() { return this.browser.close(); } diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index 234d60da9f..f29b485c84 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -79,8 +79,26 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); } - await Promise.all(sessions.map((session) => session.close())); + const performanceEntries = {}; + await Promise.all(sessions.map(async (session) => { + // Collecting all performance monitoring data before closing the session + const measurements = await session.page.evaluate(() => { + let measurements = []; + window.mxPerformanceMonitor.addPerformanceDataCallback({ + entryNames: [ + window.mxPerformanceEntryNames.REGISTER, + ], + callback: (events) => { + measurements = JSON.stringify(events); + }, + }, true); + return measurements; + }); + performanceEntries[session.username] = JSON.parse(measurements); + return session.close(); + })); + fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); if (failure) { process.exit(-1); } else {