Merge remote-tracking branch 'robintown/widget-feed-state' into element-call-nov-preview

element-call-nov-preview
Timo 2024-11-12 15:19:27 +01:00
commit 9cde9af706
40 changed files with 310 additions and 150 deletions

View File

@ -127,7 +127,7 @@
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "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", "memoize-one": "^6.0.0",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",

View File

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // 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. // 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. // 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<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -186,7 +186,7 @@ input[type="search"].mx_textinput_icon {
/* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */ /* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */
input[type="text"].mx_textinput_icon.mx_textinput_search, input[type="text"].mx_textinput_icon.mx_textinput_search,
input[type="search"].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, */ /* dont search UI as not all browsers support it, */

View File

@ -32,8 +32,8 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_DeviceExpandDetailsButton_icon { .mx_DeviceExpandDetailsButton_icon {
height: 16px; height: 24px;
width: 16px; width: 24px;
transition: all 0.3s; transition: all 0.3s;
transform: var(--icon-transform); transform: var(--icon-transform);

View File

@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: currentColor; 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-size: 100%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
float: right; float: right;

View File

@ -62,7 +62,7 @@ Please see LICENSE files in the repository root for full details.
&::before { &::before {
background-color: $info-plinth-fg-color; 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-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 50px; mask-size: 50px;

View File

@ -121,7 +121,7 @@ Please see LICENSE files in the repository root for full details.
background-color: $tertiary-content; background-color: $tertiary-content;
mask-size: 16px; mask-size: 16px;
transform: rotate(270deg); 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 { &.mx_SpaceHierarchy_subspace_toggle_shown::before {

View File

@ -48,7 +48,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $background; 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); transform: rotate(270deg);
} }
@ -169,7 +169,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: 20px; mask-size: 20px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $tertiary-content; 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 { .mx_SpaceButton_icon {

View File

@ -36,9 +36,24 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_AnalyticsLearnMore_bullets li { .mx_AnalyticsLearnMore_bullets li {
background: url("$(res)/img/tick-circle.svg") no-repeat;
list-style-type: none; list-style-type: none;
padding: 2px 0px 20px 32px; padding: 2px 0 0 32px;
margin-bottom: 20px;
vertical-align: middle; 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;
}
} }
} }

View File

@ -39,11 +39,13 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_Dropdown_arrow { .mx_Dropdown_arrow {
width: 10px; width: 16px;
height: 6px; height: 16px;
padding-right: 9px; margin-right: 4px;
mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center;
mask-size: 18px;
background: $primary-content; background: $primary-content;
} }

View File

@ -51,12 +51,15 @@ Please see LICENSE files in the repository root for full details.
.mx_Field_select::before { .mx_Field_select::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 15px; top: 50%;
right: 10px; transform: translateY(-50%);
width: 10px; right: 4px;
height: 6px; width: 18px;
mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); height: 18px;
mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $primary-content; background-color: $primary-content;
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;

View File

@ -30,6 +30,6 @@ Please see LICENSE files in the repository root for full details.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; 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); background-color: var(--cpd-color-icon-secondary);
} }

View File

@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: currentColor; 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-size: 100%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
float: right; float: right;

View File

@ -26,9 +26,9 @@ Please see LICENSE files in the repository root for full details.
height: 16px; height: 16px;
width: 16px; width: 16px;
padding: 4px; 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-repeat: no-repeat;
mask-position: 7px center; mask-position: center;
background-color: $header-panel-text-primary-color; background-color: $header-panel-text-primary-color;
} }
} }

View File

@ -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 { .mx_LinkPreviewGroup_hide:focus-visible:focus svg {
visibility: visible; visibility: visible;
} }

View File

@ -42,7 +42,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $tertiary-content; 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"] { &[aria-expanded="true"] {

View File

@ -160,7 +160,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: var(--cpd-color-icon-secondary); 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 { &.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_showMoreButtonChevron,
.mx_RoomSublist_showLessButtonChevron { .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 { .mx_RoomSublist_showLessButtonChevron {

View File

@ -67,7 +67,7 @@ Please see LICENSE files in the repository root for full details.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 2px 3px; mask-position: 2px 3px;
mask-size: 24px; 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");
} }
} }

View File

@ -147,7 +147,7 @@ Please see LICENSE files in the repository root for full details.
&::before { &::before {
content: ""; content: "";
display: inline-block; 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-size: 20px;
mask-position: center; mask-position: center;
background-color: $call-primary-content; background-color: $call-primary-content;

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7.5L9 10.5L12 7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 217 B

View File

@ -1,11 +0,0 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1378.000000, -91.000000)" stroke="#61708b" stroke-width="1">
<g id="search-copy" transform="translate(1379.000000, 92.000000)">
<circle id="Oval" cx="6.22222222" cy="6.22222222" r="6.22222222"></circle>
<path d="M14,14 L10.6166667,10.6166667" id="Path"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 674 B

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px" height="16px" viewBox="-1 -1 10 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: sketchtool 3.5.1 (25234) - http://www.bohemiancoding.com/sketch -->
<title>minimise</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="02-Chat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="02_1-Chat-collapsed-w-topic" sketch:type="MSArtboardGroup" transform="translate(-176.000000, -27.000000)" stroke-width="2" stroke="#9FA9BA">
<g id="Room-list" sketch:type="MSLayerGroup">
<g id="Room-list/Header" sketch:type="MSShapeGroup">
<g id="minimise" transform="translate(172.000000, 25.000000)">
<path d="M7,5 L15,5 L15,13" id="Path-53-Copy" transform="translate(11.000000, 9.000000) scale(-1, -1) rotate(-315.000000) translate(-11.000000, -9.000000) "></path>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2V2Z" stroke="#0DBD8B" stroke-width="2" stroke-linecap="square"/>
<path d="M6.54549 12.8882L9.80306 16.2426L17.4546 8.36377" stroke="#0DBD8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

View File

@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
import classNames from "classnames"; import classNames from "classnames";
import React, { ComponentProps } from "react"; 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 { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../elements/AccessibleButton";
@ -38,7 +38,7 @@ export const DeviceExpandDetailsButton = <T extends keyof JSX.IntrinsicElements>
})} })}
onClick={onClick} onClick={onClick}
> >
<CaretIcon className="mx_DeviceExpandDetailsButton_icon" /> <ChevronDownIcon className="mx_DeviceExpandDetailsButton_icon" />
</AccessibleButton> </AccessibleButton>
); );
}; };

View File

@ -6,7 +6,14 @@
* Please see LICENSE files in the repository root for full details. * 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 { KnownMembership } from "matrix-js-sdk/src/types";
import { import {
ClientWidgetApi, ClientWidgetApi,
@ -154,7 +161,10 @@ export class StopGapWidget extends EventEmitter {
private kind: WidgetKind; private kind: WidgetKind;
private readonly virtual: boolean; private readonly virtual: boolean;
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
private stickyPromise?: () => Promise<void>; // 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<void>;
// Holds events that should be fed to the widget once they finish decrypting
private readonly eventsToFeed = new WeakSet<MatrixEvent>();
public constructor(private appTileProps: IAppTileProps) { public constructor(private appTileProps: IAppTileProps) {
super(); super();
@ -330,6 +340,7 @@ export class StopGapWidget extends EventEmitter {
// Attach listeners for feeding events - the underlying widget classes handle permissions for us // Attach listeners for feeding events - the underlying widget classes handle permissions for us
this.client.on(ClientEvent.Event, this.onEvent); this.client.on(ClientEvent.Event, this.onEvent);
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.on(RoomStateEvent.Events, this.onStateEvent);
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
this.messaging.on( this.messaging.on(
@ -460,17 +471,33 @@ export class StopGapWidget extends EventEmitter {
this.client.off(ClientEvent.Event, this.onEvent); this.client.off(ClientEvent.Event, this.onEvent);
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.off(RoomStateEvent.Events, this.onStateEvent);
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
} }
private onEvent = (ev: MatrixEvent): void => { private onEvent = (ev: MatrixEvent): void => {
this.client.decryptEventIfNeeded(ev); this.client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; // Only process non-state events here; we don't want to confuse the
this.feedEvent(ev); // 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 => { 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); this.feedEvent(ev);
}; };
@ -480,69 +507,104 @@ export class StopGapWidget extends EventEmitter {
await this.messaging?.feedToDevice(ev.getEffectiveEvent() as IRoomEvent, ev.isEncrypted()); 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. * Determines whether the event comes from a room that we've been invited to
// If the event is after, or we don't have a marker for the room, then we'll send it through. * (in which case we likely don't have the full timeline).
// */
// This approach of "read up to" prevents widgets receiving decryption spam from startup or private isFromInvite(ev: MatrixEvent): boolean {
// receiving out-of-order events from backfill and such. const room = this.client.getRoom(ev.getRoomId());
// return room?.getMyMembership() === KnownMembership.Invite;
// 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;
}
// should be true to forward the event to the widget /**
let shouldForward = false; * Advances the "read up to" marker for a room to a certain event. No-ops if
* the event is before the marker.
const room = this.client.getRoom(ev.getRoomId()!); * @returns Whether the "read up to" marker was advanced.
if (!room) return; */
// Timelines are most recent last, so reverse the order and limit ourselves to 100 events private advanceReadUpToMarker(ev: MatrixEvent): boolean {
// 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();
const evId = ev.getId(); const evId = ev.getId();
if (evRoomId && evId) { if (evId === undefined) return false;
const room = this.client.getRoom(evRoomId); const roomId = ev.getRoomId();
if (room && room.getMyMembership() === KnownMembership.Join && !isRelationToUnknown) { if (roomId === undefined) return false;
this.readUpToMap[evRoomId] = evId; 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(); const raw = ev.getEffectiveEvent();
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
logger.error("Error sending event to widget: ", e); logger.error("Error sending event to widget: ", e);

View File

@ -24,6 +24,7 @@ import {
WidgetDriver, WidgetDriver,
WidgetEventCapability, WidgetEventCapability,
WidgetKind, WidgetKind,
IWidgetApiErrorResponseDataDetails,
ISearchUserDirectoryResult, ISearchUserDirectoryResult,
IGetMediaConfigResult, IGetMediaConfigResult,
UpdateDelayedEventAction, UpdateDelayedEventAction,
@ -33,6 +34,7 @@ import {
ITurnServer as IClientTurnServer, ITurnServer as IClientTurnServer,
EventType, EventType,
IContent, IContent,
MatrixError,
MatrixEvent, MatrixEvent,
Room, Room,
Direction, Direction,
@ -689,4 +691,15 @@ export class StopGapWidgetDriver extends WidgetDriver {
const blob = await response.blob(); const blob = await response.blob();
return { file: 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;
}
} }

View File

@ -263,9 +263,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div> </div>
</div> </div>
@ -416,9 +425,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,9 +9,18 @@ exports[`<DeviceExpandDetailsButton /> renders when expanded 1`] = `
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div>, </div>,
} }
@ -26,9 +35,18 @@ exports[`<DeviceExpandDetailsButton /> renders when not expanded 1`] = `
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div>, </div>,
} }

View File

@ -163,9 +163,18 @@ exports[`<SessionManagerTab /> current session section renders current session s
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div> </div>
</div> </div>
@ -302,9 +311,18 @@ exports[`<SessionManagerTab /> current session section renders current session s
role="button" role="button"
tabindex="0" tabindex="0"
> >
<div <svg
class="mx_DeviceExpandDetailsButton_icon" class="mx_DeviceExpandDetailsButton_icon"
/> fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details.
import { mocked, MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import { last } from "lodash"; 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 { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
import { waitFor } from "jest-matrix-react"; import { waitFor } from "jest-matrix-react";
@ -134,6 +141,46 @@ describe("StopGapWidget", () => {
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org"); 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", () => { it("should not feed incoming event if not in timeline", () => {
const event = mkEvent({ const event = mkEvent({
event: true, event: true,

View File

@ -8309,7 +8309,7 @@ matrix-events-sdk@0.0.1:
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "34.10.0" 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: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0"
@ -8320,7 +8320,7 @@ matrix-events-sdk@0.0.1:
jwt-decode "^4.0.0" jwt-decode "^4.0.0"
loglevel "^1.7.1" loglevel "^1.7.1"
matrix-events-sdk "0.0.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" oidc-client-ts "^3.0.1"
p-retry "4" p-retry "4"
sdp-transform "^2.14.1" sdp-transform "^2.14.1"
@ -8345,10 +8345,10 @@ matrix-web-i18n@^3.2.1:
minimist "^1.2.8" minimist "^1.2.8"
walk "^2.3.15" walk "^2.3.15"
matrix-widget-api@^1.8.2, matrix-widget-api@^1.9.0: matrix-widget-api@^1.10.0:
version "1.9.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw== integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"