diff --git a/package.json b/package.json index cbb8a33480..e40dea659e 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^1.9.0", + "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 5e5728a0b5..46e7f4c43b 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:d1a89bd0fcdc2bf2900dac30696d53bb9e44da1231faacd5c2d3b9f539ce9586"; +const DOCKER_TAG = "develop@sha256:b90c4e10abfc6bb4fb9301d5b148ab7e1ab752298624a705e84e7e1ad6037d08"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png index 25380a74b2..1387ef062d 100644 Binary files a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png and b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png index ab9fdb2bf6..bac041646a 100644 Binary files a/playwright/snapshots/register/register.spec.ts/registration-linux.png and b/playwright/snapshots/register/register.spec.ts/registration-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 30436d0abc..913ccf9839 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index bceaa4a283..a30e9969b6 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index bf47c91388..ef855a2da2 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 01a6c6089b..9f34ba19e3 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 31a7ed42f1..082650056f 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index d2852b7c0f..e858838ab9 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png index 26f5bfdfa9..7d8fccd672 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 05a3dac067..15ba02b6b8 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -186,7 +186,7 @@ input[type="search"].mx_textinput_icon { /* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */ input[type="text"].mx_textinput_icon.mx_textinput_search, input[type="search"].mx_textinput_icon.mx_textinput_search { - background-image: url("$(res)/img/feather-customised/search-input.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/search.svg"); } /* dont search UI as not all browsers support it, */ diff --git a/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss index de5d0bc5e6..8ad786c4ba 100644 --- a/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss +++ b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss @@ -32,8 +32,8 @@ Please see LICENSE files in the repository root for full details. } .mx_DeviceExpandDetailsButton_icon { - height: 16px; - width: 16px; + height: 24px; + width: 24px; transition: all 0.3s; transform: var(--icon-transform); diff --git a/res/css/structures/_GenericDropdownMenu.pcss b/res/css/structures/_GenericDropdownMenu.pcss index d58c29f81a..bf0098b4ed 100644 --- a/res/css/structures/_GenericDropdownMenu.pcss +++ b/res/css/structures/_GenericDropdownMenu.pcss @@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details. width: 18px; height: 18px; background: currentColor; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 100%; mask-repeat: no-repeat; float: right; diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index 359f67c803..eaa02cd2d2 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -62,7 +62,7 @@ Please see LICENSE files in the repository root for full details. &::before { background-color: $info-plinth-fg-color; - mask: url("$(res)/img/feather-customised/search-input.svg"); + mask: url("@vector-im/compound-design-tokens/icons/search.svg"); mask-repeat: no-repeat; mask-position: center; mask-size: 50px; diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 812b5474a3..ccbeef0734 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -121,7 +121,7 @@ Please see LICENSE files in the repository root for full details. background-color: $tertiary-content; mask-size: 16px; transform: rotate(270deg); - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &.mx_SpaceHierarchy_subspace_toggle_shown::before { diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 7875e62973..668dde945a 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -48,7 +48,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: $background; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); transform: rotate(270deg); } @@ -169,7 +169,7 @@ Please see LICENSE files in the repository root for full details. mask-size: 20px; mask-repeat: no-repeat; background-color: $tertiary-content; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } .mx_SpaceButton_icon { diff --git a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss index 01d69b0385..456b28d88a 100644 --- a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss +++ b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss @@ -36,9 +36,24 @@ Please see LICENSE files in the repository root for full details. } .mx_AnalyticsLearnMore_bullets li { - background: url("$(res)/img/tick-circle.svg") no-repeat; list-style-type: none; - padding: 2px 0px 20px 32px; + padding: 2px 0 0 32px; + margin-bottom: 20px; vertical-align: middle; + position: relative; + + &::before { + content: ""; + position: absolute; + width: 26px; + height: 26px; + left: 0; + top: 0; + background-color: #0dbd8b; + mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } } } diff --git a/res/css/views/elements/_Dropdown.pcss b/res/css/views/elements/_Dropdown.pcss index 7a3ebb9c29..b91af285fd 100644 --- a/res/css/views/elements/_Dropdown.pcss +++ b/res/css/views/elements/_Dropdown.pcss @@ -39,11 +39,13 @@ Please see LICENSE files in the repository root for full details. } .mx_Dropdown_arrow { - width: 10px; - height: 6px; - padding-right: 9px; - mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); + width: 16px; + height: 16px; + margin-right: 4px; + mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-repeat: no-repeat; + mask-position: center; + mask-size: 18px; background: $primary-content; } diff --git a/res/css/views/elements/_Field.pcss b/res/css/views/elements/_Field.pcss index 2659c4d389..21a0a0208a 100644 --- a/res/css/views/elements/_Field.pcss +++ b/res/css/views/elements/_Field.pcss @@ -51,12 +51,15 @@ Please see LICENSE files in the repository root for full details. .mx_Field_select::before { content: ""; position: absolute; - top: 15px; - right: 10px; - width: 10px; - height: 6px; - mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); + top: 50%; + transform: translateY(-50%); + right: 4px; + width: 18px; + height: 18px; + mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; background-color: $primary-content; z-index: 1; pointer-events: none; diff --git a/res/css/views/messages/_DateSeparator.pcss b/res/css/views/messages/_DateSeparator.pcss index 7bbf465f55..aa6f88eaaa 100644 --- a/res/css/views/messages/_DateSeparator.pcss +++ b/res/css/views/messages/_DateSeparator.pcss @@ -30,6 +30,6 @@ Please see LICENSE files in the repository root for full details. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); background-color: var(--cpd-color-icon-secondary); } diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index a9743d945b..93efded304 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details. width: 18px; height: 18px; background: currentColor; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 100%; mask-repeat: no-repeat; float: right; diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 0deb3d3708..d381d03867 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -26,9 +26,9 @@ Please see LICENSE files in the repository root for full details. height: 16px; width: 16px; padding: 4px; - mask-image: url("$(res)/img/minimise.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-left.svg"); mask-repeat: no-repeat; - mask-position: 7px center; + mask-position: center; background-color: $header-panel-text-primary-color; } } diff --git a/res/css/views/rooms/_LinkPreviewGroup.pcss b/res/css/views/rooms/_LinkPreviewGroup.pcss index e540c149b6..751a394c44 100644 --- a/res/css/views/rooms/_LinkPreviewGroup.pcss +++ b/res/css/views/rooms/_LinkPreviewGroup.pcss @@ -18,7 +18,7 @@ Please see LICENSE files in the repository root for full details. } } - &:hover .mx_LinkPreviewGroup_hide img, + &:hover .mx_LinkPreviewGroup_hide svg, .mx_LinkPreviewGroup_hide:focus-visible:focus svg { visibility: visible; } diff --git a/res/css/views/rooms/_RoomListHeader.pcss b/res/css/views/rooms/_RoomListHeader.pcss index 07aa1cbf5b..6fbd2a38db 100644 --- a/res/css/views/rooms/_RoomListHeader.pcss +++ b/res/css/views/rooms/_RoomListHeader.pcss @@ -42,7 +42,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: $tertiary-content; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &[aria-expanded="true"] { diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index d4d6f05719..a804134430 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -160,7 +160,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: var(--cpd-color-icon-secondary); - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &.mx_RoomSublist_collapseBtn_collapsed::before { @@ -276,7 +276,7 @@ Please see LICENSE files in the repository root for full details. .mx_RoomSublist_showMoreButtonChevron, .mx_RoomSublist_showLessButtonChevron { - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } .mx_RoomSublist_showLessButtonChevron { diff --git a/res/css/views/spaces/_SpaceCreateMenu.pcss b/res/css/views/spaces/_SpaceCreateMenu.pcss index e501852e29..763807d48d 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.pcss +++ b/res/css/views/spaces/_SpaceCreateMenu.pcss @@ -67,7 +67,7 @@ Please see LICENSE files in the repository root for full details. mask-repeat: no-repeat; mask-position: 2px 3px; mask-size: 24px; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } } diff --git a/res/css/views/voip/_CallView.pcss b/res/css/views/voip/_CallView.pcss index 7cb7925cd8..6a6f975710 100644 --- a/res/css/views/voip/_CallView.pcss +++ b/res/css/views/voip/_CallView.pcss @@ -147,7 +147,7 @@ Please see LICENSE files in the repository root for full details. &::before { content: ""; display: inline-block; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 20px; mask-position: center; background-color: $call-primary-content; diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg deleted file mode 100644 index a091913b42..0000000000 --- a/res/img/feather-customised/chevron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/search-input.svg b/res/img/feather-customised/search-input.svg deleted file mode 100644 index 028b84d559..0000000000 --- a/res/img/feather-customised/search-input.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/minimise.svg b/res/img/minimise.svg deleted file mode 100644 index eecf181f61..0000000000 --- a/res/img/minimise.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - minimise - Created with sketchtool. - - - - - - - - - - - - - diff --git a/res/img/tick-circle.svg b/res/img/tick-circle.svg deleted file mode 100644 index 7cedb62985..0000000000 --- a/res/img/tick-circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index d99a2e5d31..e7839b71da 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details. import classNames from "classnames"; import React, { ComponentProps } from "react"; +import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { Icon as CaretIcon } from "../../../../../res/img/feather-customised/dropdown-arrow.svg"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; @@ -38,7 +38,7 @@ export const DeviceExpandDetailsButton = })} onClick={onClick} > - + ); }; diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index e5c46e6a6a..eb82eb2730 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -6,7 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ -import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix"; +import { + Room, + MatrixEvent, + MatrixEventEvent, + MatrixClient, + ClientEvent, + RoomStateEvent, +} from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { ClientWidgetApi, @@ -154,7 +161,10 @@ export class StopGapWidget extends EventEmitter { private kind: WidgetKind; private readonly virtual: boolean; private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID - private stickyPromise?: () => Promise; // This promise will be called and needs to resolve before the widget will actually become sticky. + // This promise will be called and needs to resolve before the widget will actually become sticky. + private stickyPromise?: () => Promise; + // Holds events that should be fed to the widget once they finish decrypting + private readonly eventsToFeed = new WeakSet(); public constructor(private appTileProps: IAppTileProps) { super(); @@ -330,6 +340,7 @@ export class StopGapWidget extends EventEmitter { // Attach listeners for feeding events - the underlying widget classes handle permissions for us this.client.on(ClientEvent.Event, this.onEvent); this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(RoomStateEvent.Events, this.onStateEvent); this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.messaging.on( @@ -460,17 +471,33 @@ export class StopGapWidget extends EventEmitter { this.client.off(ClientEvent.Event, this.onEvent); this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.off(RoomStateEvent.Events, this.onStateEvent); this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } private onEvent = (ev: MatrixEvent): void => { this.client.decryptEventIfNeeded(ev); - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.feedEvent(ev); + // Only process non-state events here; we don't want to confuse the + // widget with a state event from the timeline that might not have + // actually updated the room's state + if (!ev.isState()) this.handleEvent(ev); }; private onEventDecrypted = (ev: MatrixEvent): void => { - if (ev.isDecryptionFailure()) return; + this.handleEvent(ev); + }; + + private onStateEvent = (ev: MatrixEvent): void => { + // State events get to skip all the checks of handleEvent and be fed + // directly to the widget. When it comes to state events, we don't care + // so much about getting the order and contents of the timeline right as + // we care about state updates reliably getting through to the widget so + // that it sees the same state as what the server calculated. + // TODO: We can provide widgets with a more complete timeline stream + // while also getting state updates right if we create a separate widget + // action for communicating state deltas, similar to how the 'state' + // sections of sync responses in Simplified Sliding Sync and MSC4222 + // work. this.feedEvent(ev); }; @@ -480,69 +507,104 @@ export class StopGapWidget extends EventEmitter { await this.messaging?.feedToDevice(ev.getEffectiveEvent() as IRoomEvent, ev.isEncrypted()); }; - private feedEvent(ev: MatrixEvent): void { - if (!this.messaging) return; + /** + * Determines whether the event has a relation to an unknown parent. + */ + private relatesToUnknown(ev: MatrixEvent): boolean { + // Replies to unknown events don't count + if (!ev.relationEventId || ev.replyEventId) return false; + const room = this.client.getRoom(ev.getRoomId()); + return room === null || !room.findEventById(ev.relationEventId); + } - // Check to see if this event would be before or after our "read up to" marker. If it's - // before, or we can't decide, then we assume the widget will have already seen the event. - // If the event is after, or we don't have a marker for the room, then we'll send it through. - // - // This approach of "read up to" prevents widgets receiving decryption spam from startup or - // receiving out-of-order events from backfill and such. - // - // Skip marker timeline check for events with relations to unknown parent because these - // events are not added to the timeline here and will be ignored otherwise: - // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213 - let isRelationToUnknown: boolean | undefined = undefined; - const upToEventId = this.readUpToMap[ev.getRoomId()!]; - if (upToEventId) { - // Small optimization for exact match (prevent search) - if (upToEventId === ev.getId()) { - return; - } + /** + * Determines whether the event comes from a room that we've been invited to + * (in which case we likely don't have the full timeline). + */ + private isFromInvite(ev: MatrixEvent): boolean { + const room = this.client.getRoom(ev.getRoomId()); + return room?.getMyMembership() === KnownMembership.Invite; + } - // should be true to forward the event to the widget - let shouldForward = false; - - const room = this.client.getRoom(ev.getRoomId()!); - if (!room) return; - // Timelines are most recent last, so reverse the order and limit ourselves to 100 events - // to avoid overusing the CPU. - const timeline = room.getLiveTimeline(); - const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); - - for (const timelineEvent of events) { - if (timelineEvent.getId() === upToEventId) { - break; - } else if (timelineEvent.getId() === ev.getId()) { - shouldForward = true; - break; - } - } - - if (!shouldForward) { - // checks that the event has a relation to unknown event - isRelationToUnknown = - !ev.replyEventId && !!ev.relationEventId && !room.findEventById(ev.relationEventId); - if (!isRelationToUnknown) { - // Ignore the event: it is before our interest. - return; - } - } - } - - // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from - // invitation room will assign it and new state events will be not forwarded to the widget - // because of empty timeline for invitation room and assigned marker. - const evRoomId = ev.getRoomId(); + /** + * Advances the "read up to" marker for a room to a certain event. No-ops if + * the event is before the marker. + * @returns Whether the "read up to" marker was advanced. + */ + private advanceReadUpToMarker(ev: MatrixEvent): boolean { const evId = ev.getId(); - if (evRoomId && evId) { - const room = this.client.getRoom(evRoomId); - if (room && room.getMyMembership() === KnownMembership.Join && !isRelationToUnknown) { - this.readUpToMap[evRoomId] = evId; + if (evId === undefined) return false; + const roomId = ev.getRoomId(); + if (roomId === undefined) return false; + const room = this.client.getRoom(roomId); + if (room === null) return false; + + const upToEventId = this.readUpToMap[ev.getRoomId()!]; + if (!upToEventId) { + // There's no marker yet; start it at this event + this.readUpToMap[roomId] = evId; + return true; + } + + // Small optimization for exact match (skip the search) + if (upToEventId === evId) return false; + + // Timelines are most recent last, so reverse the order and limit ourselves to 100 events + // to avoid overusing the CPU. + const timeline = room.getLiveTimeline(); + const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); + + for (const timelineEvent of events) { + if (timelineEvent.getId() === upToEventId) { + // The event must be somewhere before the "read up to" marker + return false; + } else if (timelineEvent.getId() === ev.getId()) { + // The event is after the marker; advance it + this.readUpToMap[roomId] = evId; + return true; } } + // We can't say for sure whether the widget has seen the event; let's + // just assume that it has + return false; + } + + private handleEvent(ev: MatrixEvent): void { + if ( + // If we had decided earlier to feed this event to the widget, but + // it just wasn't ready, give it another try + this.eventsToFeed.delete(ev) || + // Skip marker timeline check for events with relations to unknown parent because these + // events are not added to the timeline here and will be ignored otherwise: + // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213 + this.relatesToUnknown(ev) || + // Skip marker timeline check for rooms where membership is + // 'invite', otherwise the membership event from the invitation room + // will advance the marker and new state events will not be + // forwarded to the widget. + this.isFromInvite(ev) || + // Check whether this event would be before or after our "read up to" marker. If it's + // before, or we can't decide, then we assume the widget will have already seen the event. + // If the event is after, or we don't have a marker for the room, then the marker will advance and we'll + // send it through. + // This approach of "read up to" prevents widgets receiving decryption spam from startup or + // receiving ancient events from backfill and such. + this.advanceReadUpToMarker(ev) + ) { + // If the event is still being decrypted, remember that we want to + // feed it to the widget (even if not strictly in the order given by + // the timeline) and get back to it later + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + this.eventsToFeed.add(ev); + } else { + this.feedEvent(ev); + } + } + } + + private feedEvent(ev: MatrixEvent): void { + if (this.messaging === null) return; const raw = ev.getEffectiveEvent(); this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { logger.error("Error sending event to widget: ", e); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 09553b40ce..5bc2ac7fc0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -24,6 +24,7 @@ import { WidgetDriver, WidgetEventCapability, WidgetKind, + IWidgetApiErrorResponseDataDetails, ISearchUserDirectoryResult, IGetMediaConfigResult, UpdateDelayedEventAction, @@ -33,6 +34,7 @@ import { ITurnServer as IClientTurnServer, EventType, IContent, + MatrixError, MatrixEvent, Room, Direction, @@ -689,4 +691,15 @@ export class StopGapWidgetDriver extends WidgetDriver { const blob = await response.blob(); return { file: blob }; } + + /** + * Expresses a {@link MatrixError} as a JSON payload + * for use by Widget API error responses. + * @param error The error to handle. + * @returns The error expressed as a JSON payload, + * or undefined if it is not a {@link MatrixError}. + */ + public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined { + return error instanceof MatrixError ? { matrix_api_error: error.asWidgetApiErrorData() } : undefined; + } } diff --git a/test/unit-tests/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/unit-tests/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index 5e7b8c2ae8..74a0fb919b 100644 --- a/test/unit-tests/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/unit-tests/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -263,9 +263,18 @@ exports[` renders device and correct security card when role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
@@ -416,9 +425,18 @@ exports[` renders device and correct security card when role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
diff --git a/test/unit-tests/components/views/settings/devices/__snapshots__/DeviceExpandDetailsButton-test.tsx.snap b/test/unit-tests/components/views/settings/devices/__snapshots__/DeviceExpandDetailsButton-test.tsx.snap index 5878dc11ee..5032bd2a64 100644 --- a/test/unit-tests/components/views/settings/devices/__snapshots__/DeviceExpandDetailsButton-test.tsx.snap +++ b/test/unit-tests/components/views/settings/devices/__snapshots__/DeviceExpandDetailsButton-test.tsx.snap @@ -9,9 +9,18 @@ exports[` renders when expanded 1`] = ` role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
, } @@ -26,9 +35,18 @@ exports[` renders when not expanded 1`] = ` role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
, } diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index 2c868f92e6..38f9e483c8 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -163,9 +163,18 @@ exports[` current session section renders current session s role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
@@ -302,9 +311,18 @@ exports[` current session section renders current session s role="button" tabindex="0" > -
+ fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
diff --git a/test/unit-tests/stores/widgets/StopGapWidget-test.ts b/test/unit-tests/stores/widgets/StopGapWidget-test.ts index 84cc84ee83..397c289d22 100644 --- a/test/unit-tests/stores/widgets/StopGapWidget-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidget-test.ts @@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details. import { mocked, MockedObject } from "jest-mock"; import { last } from "lodash"; -import { MatrixEvent, MatrixClient, ClientEvent, EventTimeline } from "matrix-js-sdk/src/matrix"; +import { + MatrixEvent, + MatrixClient, + ClientEvent, + EventTimeline, + EventType, + MatrixEventEvent, +} from "matrix-js-sdk/src/matrix"; import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api"; import { waitFor } from "jest-matrix-react"; @@ -134,6 +141,46 @@ describe("StopGapWidget", () => { expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org"); }); + it("feeds decrypted events asynchronously", async () => { + const event1Encrypted = new MatrixEvent({ + event_id: event1.getId(), + type: EventType.RoomMessageEncrypted, + sender: event1.sender?.userId, + room_id: event1.getRoomId(), + content: {}, + }); + const decryptingSpy1 = jest.spyOn(event1Encrypted, "isBeingDecrypted").mockReturnValue(true); + client.emit(ClientEvent.Event, event1Encrypted); + const event2Encrypted = new MatrixEvent({ + event_id: event2.getId(), + type: EventType.RoomMessageEncrypted, + sender: event2.sender?.userId, + room_id: event2.getRoomId(), + content: {}, + }); + const decryptingSpy2 = jest.spyOn(event2Encrypted, "isBeingDecrypted").mockReturnValue(true); + client.emit(ClientEvent.Event, event2Encrypted); + expect(messaging.feedEvent).not.toHaveBeenCalled(); + + // "Decrypt" the events, but in reverse order; first event 2… + event2Encrypted.event.type = event2.getType(); + event2Encrypted.event.content = event2.getContent(); + decryptingSpy2.mockReturnValue(false); + client.emit(MatrixEventEvent.Decrypted, event2Encrypted); + expect(messaging.feedEvent).toHaveBeenCalledTimes(1); + expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org"); + // …then event 1 + event1Encrypted.event.type = event1.getType(); + event1Encrypted.event.content = event1.getContent(); + decryptingSpy1.mockReturnValue(false); + client.emit(MatrixEventEvent.Decrypted, event1Encrypted); + // The events should be fed in that same order so that event 2 + // doesn't have to be blocked on the decryption of event 1 (or + // worse, dropped) + expect(messaging.feedEvent).toHaveBeenCalledTimes(2); + expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org"); + }); + it("should not feed incoming event if not in timeline", () => { const event = mkEvent({ event: true, diff --git a/yarn.lock b/yarn.lock index 343a0f3ef4..c5ef6a55e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8309,7 +8309,7 @@ matrix-events-sdk@0.0.1: "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "34.10.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/5a1488ebd5552817b1e95265afe7b3baac1231a2" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6855ace6422082d173438cb23368d2fabc6a1086" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" @@ -8320,7 +8320,7 @@ matrix-events-sdk@0.0.1: jwt-decode "^4.0.0" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.8.2" + matrix-widget-api "^1.10.0" oidc-client-ts "^3.0.1" p-retry "4" sdp-transform "^2.14.1" @@ -8345,10 +8345,10 @@ matrix-web-i18n@^3.2.1: minimist "^1.2.8" walk "^2.3.15" -matrix-widget-api@^1.8.2, matrix-widget-api@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f" - integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw== +matrix-widget-api@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" + integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== dependencies: "@types/events" "^3.0.0" events "^3.2.0"