Merge branch 'master' into develop

This commit is contained in:
RiotRobot 2023-03-28 14:30:52 +01:00
commit 0475e7107f
15 changed files with 186 additions and 125 deletions

View file

@ -1,3 +1,10 @@
Changes in [3.69.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.69.0) (2023-03-28)
=====================================================================================================
## 🐛 Bug Fixes
* Changes for matrix-js-sdk v24.0.0
* Changes for matrix-react-sdk v3.69.0
Changes in [3.68.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.68.0) (2023-03-15) Changes in [3.68.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.68.0) (2023-03-15)
===================================================================================================== =====================================================================================================

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.68.0", "version": "3.69.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -233,7 +233,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// This is recomputed on each render. It's only stored on the component // This is recomputed on each render. It's only stored on the component
// for ease of passing the data around since it's computed in one pass // for ease of passing the data around since it's computed in one pass
// over all events. // over all events.
private readReceiptsByEvent: Record<string, IReadReceiptProps[]> = {}; private readReceiptsByEvent: Map<string, IReadReceiptProps[]> = new Map();
// Track read receipts by user ID. For each user ID we've ever shown a // Track read receipts by user ID. For each user ID we've ever shown a
// a read receipt for, we store an object: // a read receipt for, we store an object:
@ -252,7 +252,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// This is recomputed on each render, using the data from the previous // This is recomputed on each render, using the data from the previous
// render as our fallback for any user IDs we can't match a receipt to a // render as our fallback for any user IDs we can't match a receipt to a
// displayed event in the current render cycle. // displayed event in the current render cycle.
private readReceiptsByUserId: Record<string, IReadReceiptForUser> = {}; private readReceiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
private readonly _showHiddenEvents: boolean; private readonly _showHiddenEvents: boolean;
private isMounted = false; private isMounted = false;
@ -637,7 +637,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Note: the EventTile might still render a "sent/sending receipt" independent of // Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely // this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often. // to assume that sent receipts are to be shown more often.
this.readReceiptsByEvent = {}; this.readReceiptsByEvent = new Map();
if (this.props.showReadReceipts) { if (this.props.showReadReceipts) {
this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(events); this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(events);
} }
@ -748,7 +748,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const eventId = mxEv.getId(); const eventId = mxEv.getId();
const highlight = eventId === this.props.highlightedEventId; const highlight = eventId === this.props.highlightedEventId;
const readReceipts = this.readReceiptsByEvent[eventId]; const readReceipts = this.readReceiptsByEvent.get(eventId);
let isLastSuccessful = false; let isLastSuccessful = false;
const isSentState = (s: EventStatus | null): boolean => !s || s === EventStatus.SENT; const isSentState = (s: EventStatus | null): boolean => !s || s === EventStatus.SENT;
@ -865,28 +865,22 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Get an object that maps from event ID to a list of read receipts that // Get an object that maps from event ID to a list of read receipts that
// should be shown next to that event. If a hidden event has read receipts, // should be shown next to that event. If a hidden event has read receipts,
// they are folded into the receipts of the last shown event. // they are folded into the receipts of the last shown event.
private getReadReceiptsByShownEvent(events: EventAndShouldShow[]): Record<string, IReadReceiptProps[]> { private getReadReceiptsByShownEvent(events: EventAndShouldShow[]): Map<string, IReadReceiptProps[]> {
const receiptsByEvent: Record<string, IReadReceiptProps[]> = {}; const receiptsByEvent: Map<string, IReadReceiptProps[]> = new Map();
const receiptsByUserId: Record< const receiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
string,
{
lastShownEventId: string;
receipt: IReadReceiptProps;
}
> = {};
let lastShownEventId; let lastShownEventId: string;
for (const { event, shouldShow } of events) { for (const event of this.props.events) {
if (shouldShow) { if (this.shouldShowEvent(event)) {
lastShownEventId = event.getId(); lastShownEventId = event.getId();
} }
if (!lastShownEventId) { if (!lastShownEventId) {
continue; continue;
} }
const existingReceipts = receiptsByEvent[lastShownEventId] || []; const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
const newReceipts = this.getReadReceiptsForEvent(event); const newReceipts = this.getReadReceiptsForEvent(event);
receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); receiptsByEvent.set(lastShownEventId, existingReceipts.concat(newReceipts));
// Record these receipts along with their last shown event ID for // Record these receipts along with their last shown event ID for
// each associated user ID. // each associated user ID.
@ -904,21 +898,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// someone which had one in the last. By looking through our previous // someone which had one in the last. By looking through our previous
// mapping of receipts by user ID, we can cover recover any receipts // mapping of receipts by user ID, we can cover recover any receipts
// that would have been lost by using the same event ID from last time. // that would have been lost by using the same event ID from last time.
for (const userId in this.readReceiptsByUserId) { for (const userId of this.readReceiptsByUserId.keys()) {
if (receiptsByUserId[userId]) { if (receiptsByUserId.get(userId)) {
continue; continue;
} }
const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; const { lastShownEventId, receipt } = this.readReceiptsByUserId.get(userId);
const existingReceipts = receiptsByEvent[lastShownEventId] || []; const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); receiptsByEvent.set(lastShownEventId, existingReceipts.concat(receipt));
receiptsByUserId[userId] = { lastShownEventId, receipt }; receiptsByUserId.set(userId, { lastShownEventId, receipt });
} }
this.readReceiptsByUserId = receiptsByUserId; this.readReceiptsByUserId = receiptsByUserId;
// After grouping receipts by shown events, do another pass to sort each // After grouping receipts by shown events, do another pass to sort each
// receipt list. // receipt list.
for (const eventId in receiptsByEvent) { for (const receipts of receiptsByEvent.values()) {
receiptsByEvent[eventId].sort((r1, r2) => { receipts.sort((r1, r2) => {
return r2.ts - r1.ts; return r2.ts - r1.ts;
}); });
} }

View file

@ -52,7 +52,7 @@ export const RoomAccountDataEventEditor: React.FC<IEditorProps> = ({ mxEvent, on
}; };
interface IProps extends IDevtoolsProps { interface IProps extends IDevtoolsProps {
events: Record<string, MatrixEvent>; events: Map<string, MatrixEvent>;
Editor: React.FC<IEditorProps>; Editor: React.FC<IEditorProps>;
actionLabel: string; actionLabel: string;
} }
@ -75,7 +75,7 @@ const BaseAccountDataExplorer: React.FC<IProps> = ({ events, Editor, actionLabel
return ( return (
<BaseTool onBack={onBack} actionLabel={actionLabel} onAction={onAction}> <BaseTool onBack={onBack} actionLabel={actionLabel} onAction={onAction}>
<FilteredList query={query} onChange={setQuery}> <FilteredList query={query} onChange={setQuery}>
{Object.entries(events).map(([eventType, ev]) => { {Array.from(events.entries()).map(([eventType, ev]) => {
const onClick = (): void => { const onClick = (): void => {
setEvent(ev); setEvent(ev);
}; };

View file

@ -21,6 +21,7 @@ import counterpart from "counterpart";
import React from "react"; import React from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { MapWithDefault, safeSet } from "matrix-js-sdk/src/utils";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
@ -629,21 +630,16 @@ export class CustomTranslationOptions {
function doRegisterTranslations(customTranslations: ICustomTranslations): void { function doRegisterTranslations(customTranslations: ICustomTranslations): void {
// We convert the operator-friendly version into something counterpart can // We convert the operator-friendly version into something counterpart can
// consume. // consume.
const langs: { // Map: lang → Record: string → translation
// same structure, just flipped key order const langs: MapWithDefault<string, Record<string, string>> = new MapWithDefault(() => ({}));
[lang: string]: {
[str: string]: string;
};
} = {};
for (const [str, translations] of Object.entries(customTranslations)) { for (const [str, translations] of Object.entries(customTranslations)) {
for (const [lang, newStr] of Object.entries(translations)) { for (const [lang, newStr] of Object.entries(translations)) {
if (!langs[lang]) langs[lang] = {}; safeSet(langs.getOrCreate(lang), str, newStr);
langs[lang][str] = newStr;
} }
} }
// Finally, tell counterpart about our translations // Finally, tell counterpart about our translations
for (const [lang, translations] of Object.entries(langs)) { for (const [lang, translations] of langs) {
counterpart.registerTranslations(lang, translations); counterpart.registerTranslations(lang, translations);
} }
} }

View file

@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { safeSet } from "matrix-js-sdk/src/utils";
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations"; import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types"; import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types";
import { AppModule } from "./AppModule"; import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory"; import { ModuleFactory } from "./ModuleFactory";
import "./ModuleComponents"; import "./ModuleComponents";
/** /**
@ -53,9 +55,10 @@ export class ModuleRunner {
if (!i18n) continue; if (!i18n) continue;
for (const [lang, strings] of Object.entries(i18n)) { for (const [lang, strings] of Object.entries(i18n)) {
if (!merged[lang]) merged[lang] = {}; safeSet(merged, lang, merged[lang] || {});
for (const [str, val] of Object.entries(strings)) { for (const [str, val] of Object.entries(strings)) {
merged[lang][str] = val; safeSet(merged[lang], str, val);
} }
} }
} }

View file

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { safeSet } from "matrix-js-sdk/src/utils";
import { SettingLevel } from "../SettingLevel"; import { SettingLevel } from "../SettingLevel";
import { WatchManager } from "../WatchManager"; import { WatchManager } from "../WatchManager";
import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler";
@ -48,7 +50,7 @@ export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSetti
let value = this.read("mx_local_settings"); let value = this.read("mx_local_settings");
if (!value) value = {}; if (!value) value = {};
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; safeSet(value["blacklistUnverifiedDevicesPerRoom"], roomId, newValue);
this.setObject("mx_local_settings", value); this.setObject("mx_local_settings", value);
this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue);
return Promise.resolve(); return Promise.resolve();

View file

@ -135,9 +135,10 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
...eventInfo, ...eventInfo,
recipient_rageshake: rageshakeURL, recipient_rageshake: rageshakeURL,
}; };
this.matrixClient.sendToDevice(AUTO_RS_REQUEST, { this.matrixClient.sendToDevice(
[messageContent.user_id]: { [messageContent.device_id]: messageContent }, AUTO_RS_REQUEST,
}); new Map([["messageContent.user_id", new Map([[messageContent.device_id, messageContent]])]]),
);
} }
} }

View file

@ -277,7 +277,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
if (deviceId === "*") { if (deviceId === "*") {
// Send the message to all devices we have keys for // Send the message to all devices we have keys for
await client.encryptAndSendToDevices( await client.encryptAndSendToDevices(
Object.values(deviceInfoMap[userId]).map((deviceInfo) => ({ Array.from(deviceInfoMap.get(userId).values()).map((deviceInfo) => ({
userId, userId,
deviceInfo, deviceInfo,
})), })),
@ -286,7 +286,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
} else { } else {
// Send the message to a specific device // Send the message to a specific device
await client.encryptAndSendToDevices( await client.encryptAndSendToDevices(
[{ userId, deviceInfo: deviceInfoMap[userId][deviceId] }], [{ userId, deviceInfo: deviceInfoMap.get(userId).get(deviceId) }],
content, content,
); );
} }

View file

@ -17,7 +17,8 @@
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { compare } from "matrix-js-sdk/src/utils"; import { Optional } from "matrix-events-sdk";
import { compare, MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import WidgetStore, { IApp } from "../WidgetStore"; import WidgetStore, { IApp } from "../WidgetStore";
@ -91,19 +92,17 @@ export const MAX_PINNED = 3;
const MIN_WIDGET_WIDTH_PCT = 10; // 10% const MIN_WIDGET_WIDTH_PCT = 10; // 10%
const MIN_WIDGET_HEIGHT_PCT = 2; // 2% const MIN_WIDGET_HEIGHT_PCT = 2; // 2%
interface ContainerValue {
ordered: IApp[];
height?: number;
distributions?: number[];
}
export class WidgetLayoutStore extends ReadyWatchingStore { export class WidgetLayoutStore extends ReadyWatchingStore {
private static internalInstance: WidgetLayoutStore; private static internalInstance: WidgetLayoutStore;
private byRoom: { // Map: room Id → container → ContainerValue
[roomId: string]: Partial<{ private byRoom: MapWithDefault<string, Map<Container, ContainerValue>> = new MapWithDefault(() => new Map());
[container in Container]: {
ordered: IApp[];
height?: number | null;
distributions?: number[];
};
}>;
} = {};
private pinnedRef: string | undefined; private pinnedRef: string | undefined;
private layoutRef: string | undefined; private layoutRef: string | undefined;
private dynamicRef: string | undefined; private dynamicRef: string | undefined;
@ -143,7 +142,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
protected async onNotReady(): Promise<void> { protected async onNotReady(): Promise<void> {
this.byRoom = {}; this.byRoom = new MapWithDefault(() => new Map());
this.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState); this.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState);
if (this.pinnedRef) SettingsStore.unwatchSetting(this.pinnedRef); if (this.pinnedRef) SettingsStore.unwatchSetting(this.pinnedRef);
@ -155,7 +154,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
private updateAllRooms = (): void => { private updateAllRooms = (): void => {
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
if (!this.matrixClient) return; if (!this.matrixClient) return;
this.byRoom = {}; this.byRoom = new MapWithDefault(() => new Map());
for (const room of this.matrixClient.getVisibleRooms(msc3946ProcessDynamicPredecessor)) { for (const room of this.matrixClient.getVisibleRooms(msc3946ProcessDynamicPredecessor)) {
this.recalculateRoom(room); this.recalculateRoom(room);
} }
@ -194,12 +193,13 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
public recalculateRoom(room: Room): void { public recalculateRoom(room: Room): void {
const widgets = WidgetStore.instance.getApps(room.roomId); const widgets = WidgetStore.instance.getApps(room.roomId);
if (!widgets?.length) { if (!widgets?.length) {
this.byRoom[room.roomId] = {}; this.byRoom.set(room.roomId, new Map());
this.emitFor(room); this.emitFor(room);
return; return;
} }
const beforeChanges = JSON.stringify(this.byRoom[room.roomId]); const roomContainers = this.byRoom.getOrCreate(room.roomId);
const beforeChanges = JSON.stringify(recursiveMapToObject(roomContainers));
const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, ""); const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, "");
const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId); const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId);
@ -335,33 +335,35 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
// Finally, fill in our cache and update // Finally, fill in our cache and update
this.byRoom[room.roomId] = {}; const newRoomContainers = new Map();
this.byRoom.set(room.roomId, newRoomContainers);
if (topWidgets.length) { if (topWidgets.length) {
this.byRoom[room.roomId][Container.Top] = { newRoomContainers.set(Container.Top, {
ordered: topWidgets, ordered: topWidgets,
distributions: widths, distributions: widths,
height: maxHeight, height: maxHeight,
}; });
} }
if (rightWidgets.length) { if (rightWidgets.length) {
this.byRoom[room.roomId][Container.Right] = { newRoomContainers.set(Container.Right, {
ordered: rightWidgets, ordered: rightWidgets,
}; });
} }
if (centerWidgets.length) { if (centerWidgets.length) {
this.byRoom[room.roomId][Container.Center] = { newRoomContainers.set(Container.Center, {
ordered: centerWidgets, ordered: centerWidgets,
}; });
} }
const afterChanges = JSON.stringify(this.byRoom[room.roomId]); const afterChanges = JSON.stringify(recursiveMapToObject(newRoomContainers));
if (afterChanges !== beforeChanges) { if (afterChanges !== beforeChanges) {
this.emitFor(room); this.emitFor(room);
} }
} }
public getContainerWidgets(room: Room, container: Container): IApp[] { public getContainerWidgets(room: Optional<Room>, container: Container): IApp[] {
return this.byRoom[room.roomId]?.[container]?.ordered || []; return this.byRoom.get(room?.roomId)?.get(container)?.ordered || [];
} }
public isInContainer(room: Room, widget: IApp, container: Container): boolean { public isInContainer(room: Room, widget: IApp, container: Container): boolean {
@ -381,7 +383,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
public getResizerDistributions(room: Room, container: Container): string[] { public getResizerDistributions(room: Room, container: Container): string[] {
// yes, string. // yes, string.
let distributions = this.byRoom[room.roomId]?.[container]?.distributions; let distributions = this.byRoom.get(room.roomId)?.get(container)?.distributions;
if (!distributions || distributions.length < 2) return []; if (!distributions || distributions.length < 2) return [];
// The distributor actually expects to be fed N-1 sizes and expands the middle section // The distributor actually expects to be fed N-1 sizes and expands the middle section
@ -410,19 +412,19 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
container: container, container: container,
width: numbers[i], width: numbers[i],
index: i, index: i,
height: this.byRoom[room.roomId]?.[container]?.height || MIN_WIDGET_HEIGHT_PCT, height: this.byRoom.get(room.roomId)?.get(container)?.height || MIN_WIDGET_HEIGHT_PCT,
}; };
}); });
this.updateUserLayout(room, localLayout); this.updateUserLayout(room, localLayout);
} }
public getContainerHeight(room: Room, container: Container): number | null { public getContainerHeight(room: Room, container: Container): number | null {
return this.byRoom[room.roomId]?.[container]?.height ?? null; // let the default get returned if needed return this.byRoom.get(room.roomId)?.get(container)?.height ?? null; // let the default get returned if needed
} }
public setContainerHeight(room: Room, container: Container, height?: number | null): void { public setContainerHeight(room: Room, container: Container, height?: number | null): void {
const widgets = this.getContainerWidgets(room, container); const widgets = this.getContainerWidgets(room, container);
const widths = this.byRoom[room.roomId]?.[container]?.distributions; const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
const localLayout: Record<string, IStoredLayout> = {}; const localLayout: Record<string, IStoredLayout> = {};
widgets.forEach((w, i) => { widgets.forEach((w, i) => {
localLayout[w.id] = { localLayout[w.id] = {
@ -444,8 +446,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
const newIdx = clamp(currentIdx + delta, 0, widgets.length); const newIdx = clamp(currentIdx + delta, 0, widgets.length);
widgets.splice(newIdx, 0, widget); widgets.splice(newIdx, 0, widget);
const widths = this.byRoom[room.roomId]?.[container]?.distributions; const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
const height = this.byRoom[room.roomId]?.[container]?.height; const height = this.byRoom.get(room.roomId)?.get(container)?.height;
const localLayout: Record<string, IStoredLayout> = {}; const localLayout: Record<string, IStoredLayout> = {};
widgets.forEach((w, i) => { widgets.forEach((w, i) => {
localLayout[w.id] = { localLayout[w.id] = {
@ -512,8 +514,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
if (container === Container.Top) { if (container === Container.Top) {
const containerWidgets = this.getContainerWidgets(room, container); const containerWidgets = this.getContainerWidgets(room, container);
const idx = containerWidgets.findIndex((w) => w.id === widget.id); const idx = containerWidgets.findIndex((w) => w.id === widget.id);
const widths = this.byRoom[room.roomId]?.[container]?.distributions; const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
const height = this.byRoom[room.roomId]?.[container]?.height; const height = this.byRoom.get(room.roomId)?.get(container)?.height;
evContent.widgets[widget.id] = { evContent.widgets[widget.id] = {
...evContent.widgets[widget.id], ...evContent.widgets[widget.id],
height: height ? Math.round(height) : undefined, height: height ? Math.round(height) : undefined,
@ -526,12 +528,12 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
private getAllWidgets(room: Room): [IApp, Container][] { private getAllWidgets(room: Room): [IApp, Container][] {
const containers = this.byRoom[room.roomId]; const containers = this.byRoom.get(room.roomId);
if (!containers) return []; if (!containers) return [];
const ret: [IApp, Container][] = []; const ret: [IApp, Container][] = [];
for (const container in containers) { for (const [container, containerValue] of containers) {
const widgets = containers[container as Container]!.ordered; const widgets = containerValue.ordered;
for (const widget of widgets) { for (const widget of widgets) {
ret.push([widget, container as Container]); ret.push([widget, container as Container]);
} }
@ -545,12 +547,12 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
for (const [widget, container] of allWidgets) { for (const [widget, container] of allWidgets) {
const containerWidgets = this.getContainerWidgets(room, container); const containerWidgets = this.getContainerWidgets(room, container);
const idx = containerWidgets.findIndex((w) => w.id === widget.id); const idx = containerWidgets.findIndex((w) => w.id === widget.id);
const widths = this.byRoom[room.roomId]?.[container]?.distributions; const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
if (!newLayout[widget.id]) { if (!newLayout[widget.id]) {
newLayout[widget.id] = { newLayout[widget.id] = {
container: container, container: container,
index: idx, index: idx,
height: this.byRoom[room.roomId]?.[container]?.height, height: this.byRoom.get(room.roomId)?.get(container)?.height,
width: widths?.[idx], width: widths?.[idx],
}; };
} }

View file

@ -71,7 +71,7 @@ export const recordClientInformation = async (
* client information for devices NOT in this list will be removed * client information for devices NOT in this list will be removed
*/ */
export const pruneClientInformation = (validDeviceIds: string[], matrixClient: MatrixClient): void => { export const pruneClientInformation = (validDeviceIds: string[], matrixClient: MatrixClient): void => {
Object.values(matrixClient.store.accountData).forEach((event) => { Array.from(matrixClient.store.accountData.values()).forEach((event) => {
if (!event.getType().startsWith(clientInformationEventPrefix)) { if (!event.getType().startsWith(clientInformationEventPrefix)) {
return; return;
} }

View file

@ -76,7 +76,10 @@ describe("<SessionManagerTab />", () => {
const mockCrossSigningInfo = { const mockCrossSigningInfo = {
checkDeviceTrust: jest.fn(), checkDeviceTrust: jest.fn(),
}; };
const mockVerificationRequest = { cancel: jest.fn(), on: jest.fn() } as unknown as VerificationRequest; const mockVerificationRequest = {
cancel: jest.fn(),
on: jest.fn(),
} as unknown as VerificationRequest;
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(aliceId), ...mockClientMethodsUser(aliceId),
getStoredCrossSigningForUser: jest.fn().mockReturnValue(mockCrossSigningInfo), getStoredCrossSigningForUser: jest.fn().mockReturnValue(mockCrossSigningInfo),
@ -185,7 +188,7 @@ describe("<SessionManagerTab />", () => {
}); });
// @ts-ignore mock // @ts-ignore mock
mockClient.store = { accountData: {} }; mockClient.store = { accountData: new Map() };
mockClient.getAccountData.mockReset().mockImplementation((eventType) => { mockClient.getAccountData.mockReset().mockImplementation((eventType) => {
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
@ -222,7 +225,9 @@ describe("<SessionManagerTab />", () => {
it("does not fail when checking device verification fails", async () => { it("does not fail when checking device verification fails", async () => {
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const noCryptoError = new Error("End-to-end encryption disabled"); const noCryptoError = new Error("End-to-end encryption disabled");
mockClient.getStoredDevice.mockImplementation(() => { mockClient.getStoredDevice.mockImplementation(() => {
throw noCryptoError; throw noCryptoError;
@ -277,7 +282,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("extends device with client information when available", async () => { it("extends device with client information when available", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
mockClient.getAccountData.mockImplementation((eventType: string) => { mockClient.getAccountData.mockImplementation((eventType: string) => {
const content = { const content = {
name: "Element Web", name: "Element Web",
@ -305,7 +312,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("renders devices without available client information without error", async () => { it("renders devices without available client information without error", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const { getByTestId, queryByTestId } = render(getComponent()); const { getByTestId, queryByTestId } = render(getComponent());
@ -343,7 +352,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("goes to filtered list from security recommendations", async () => { it("goes to filtered list from security recommendations", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const { getByTestId, container } = render(getComponent()); const { getByTestId, container } = render(getComponent());
await act(async () => { await act(async () => {
@ -376,7 +387,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("renders current session section with an unverified session", async () => { it("renders current session section with an unverified session", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
await act(async () => { await act(async () => {
@ -387,7 +400,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("opens encryption setup dialog when verifiying current session", async () => { it("opens encryption setup dialog when verifiying current session", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
const modalSpy = jest.spyOn(Modal, "createDialog"); const modalSpy = jest.spyOn(Modal, "createDialog");
@ -402,7 +417,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("renders current session section with a verified session", async () => { it("renders current session section with a verified session", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id)); mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id));
mockCrossSigningInfo.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, true, false, false)); mockCrossSigningInfo.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, true, false, false));
@ -416,7 +433,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("expands current session details", async () => { it("expands current session details", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
await act(async () => { await act(async () => {
@ -500,7 +519,9 @@ describe("<SessionManagerTab />", () => {
const modalSpy = jest.spyOn(Modal, "createDialog"); const modalSpy = jest.spyOn(Modal, "createDialog");
// make the current device verified // make the current device verified
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => { mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => {
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
@ -525,7 +546,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("does not allow device verification on session that do not support encryption", async () => { it("does not allow device verification on session that do not support encryption", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => { mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => {
// current session verified = able to verify other sessions // current session verified = able to verify other sessions
@ -557,7 +580,9 @@ describe("<SessionManagerTab />", () => {
const modalSpy = jest.spyOn(Modal, "createDialog"); const modalSpy = jest.spyOn(Modal, "createDialog");
// make the current device verified // make the current device verified
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice],
});
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => { mockCrossSigningInfo.checkDeviceTrust.mockImplementation((_userId, { deviceId }) => {
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
@ -595,7 +620,9 @@ describe("<SessionManagerTab />", () => {
it("Signs out of current device", async () => { it("Signs out of current device", async () => {
const modalSpy = jest.spyOn(Modal, "createDialog"); const modalSpy = jest.spyOn(Modal, "createDialog");
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice],
});
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
await act(async () => { await act(async () => {
@ -614,7 +641,9 @@ describe("<SessionManagerTab />", () => {
it("Signs out of current device from kebab menu", async () => { it("Signs out of current device from kebab menu", async () => {
const modalSpy = jest.spyOn(Modal, "createDialog"); const modalSpy = jest.spyOn(Modal, "createDialog");
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice],
});
const { getByTestId, getByLabelText } = render(getComponent()); const { getByTestId, getByLabelText } = render(getComponent());
await act(async () => { await act(async () => {
@ -629,7 +658,9 @@ describe("<SessionManagerTab />", () => {
}); });
it("does not render sign out other devices option when only one device", async () => { it("does not render sign out other devices option when only one device", async () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] }); mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice],
});
const { getByTestId, queryByLabelText } = render(getComponent()); const { getByTestId, queryByLabelText } = render(getComponent());
await act(async () => { await act(async () => {
@ -671,9 +702,7 @@ describe("<SessionManagerTab />", () => {
// @ts-ignore setup mock // @ts-ignore setup mock
mockClient.store = { mockClient.store = {
// @ts-ignore setup mock // @ts-ignore setup mock
accountData: { accountData: new Map([[mobileDeviceClientInfo.getType(), mobileDeviceClientInfo]]),
[mobileDeviceClientInfo.getType()]: mobileDeviceClientInfo,
},
}; };
mockClient.getDevices mockClient.getDevices
@ -703,7 +732,10 @@ describe("<SessionManagerTab />", () => {
}); });
describe("other devices", () => { describe("other devices", () => {
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } }; const interactiveAuthError = {
httpStatus: 401,
data: { flows: [{ stages: ["m.login.password"] }] },
};
beforeEach(() => { beforeEach(() => {
mockClient.deleteMultipleDevices.mockReset(); mockClient.deleteMultipleDevices.mockReset();
@ -712,9 +744,13 @@ describe("<SessionManagerTab />", () => {
it("deletes a device when interactive auth is not required", async () => { it("deletes a device when interactive auth is not required", async () => {
mockClient.deleteMultipleDevices.mockResolvedValue({}); mockClient.deleteMultipleDevices.mockResolvedValue({});
mockClient.getDevices mockClient.getDevices
.mockResolvedValueOnce({ devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice] }) .mockResolvedValueOnce({
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
})
// pretend it was really deleted on refresh // pretend it was really deleted on refresh
.mockResolvedValueOnce({ devices: [alicesDevice, alicesOlderMobileDevice] }); .mockResolvedValueOnce({
devices: [alicesDevice, alicesOlderMobileDevice],
});
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
@ -785,9 +821,13 @@ describe("<SessionManagerTab />", () => {
.mockResolvedValueOnce({}); .mockResolvedValueOnce({});
mockClient.getDevices mockClient.getDevices
.mockResolvedValueOnce({ devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice] }) .mockResolvedValueOnce({
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
})
// pretend it was really deleted on refresh // pretend it was really deleted on refresh
.mockResolvedValueOnce({ devices: [alicesDevice, alicesOlderMobileDevice] }); .mockResolvedValueOnce({
devices: [alicesDevice, alicesOlderMobileDevice],
});
const { getByTestId, getByLabelText } = render(getComponent()); const { getByTestId, getByLabelText } = render(getComponent());
@ -821,7 +861,9 @@ describe("<SessionManagerTab />", () => {
// fill password and submit for interactive auth // fill password and submit for interactive auth
act(() => { act(() => {
fireEvent.change(getByLabelText("Password"), { target: { value: "topsecret" } }); fireEvent.change(getByLabelText("Password"), {
target: { value: "topsecret" },
});
fireEvent.submit(getByLabelText("Password")); fireEvent.submit(getByLabelText("Password"));
}); });
@ -1062,7 +1104,9 @@ describe("<SessionManagerTab />", () => {
await updateDeviceName(getByTestId, alicesDevice, ""); await updateDeviceName(getByTestId, alicesDevice, "");
expect(mockClient.setDeviceDetails).toHaveBeenCalledWith(alicesDevice.device_id, { display_name: "" }); expect(mockClient.setDeviceDetails).toHaveBeenCalledWith(alicesDevice.device_id, {
display_name: "",
});
}); });
it("displays an error when session display name fails to save", async () => { it("displays an error when session display name fails to save", async () => {

View file

@ -16,7 +16,7 @@ limitations under the License.
import { mocked, Mocked } from "jest-mock"; import { mocked, Mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { IDevice } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { RoomType } from "matrix-js-sdk/src/@types/event"; import { RoomType } from "matrix-js-sdk/src/@types/event";
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-utils"; import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-utils";
@ -147,12 +147,16 @@ describe("createRoom", () => {
}); });
describe("canEncryptToAllUsers", () => { describe("canEncryptToAllUsers", () => {
const trueUser = { const trueUser = new Map([
"@goodUser:localhost": { [
DEV1: {} as unknown as IDevice, "@goodUser:localhost",
DEV2: {} as unknown as IDevice, new Map([
}, ["DEV1", {} as unknown as DeviceInfo],
}; ["DEV2", {} as unknown as DeviceInfo],
]),
],
]);
const falseUser = { const falseUser = {
"@badUser:localhost": {}, "@badUser:localhost": {},
}; };

View file

@ -79,9 +79,9 @@ describe("AutoRageshakeStore", () => {
[ [
[ [
"im.vector.auto_rs_request", "im.vector.auto_rs_request",
{ Map {
"@userId:matrix.org": { "messageContent.user_id" => Map {
"undefined": { undefined => {
"device_id": undefined, "device_id": undefined,
"event_id": "utd_event_id", "event_id": "utd_event_id",
"recipient_rageshake": undefined, "recipient_rageshake": undefined,

View file

@ -185,10 +185,18 @@ describe("StopGapWidgetDriver", () => {
const aliceMobile = new DeviceInfo("aliceMobile"); const aliceMobile = new DeviceInfo("aliceMobile");
const bobDesktop = new DeviceInfo("bobDesktop"); const bobDesktop = new DeviceInfo("bobDesktop");
mocked(client.crypto!.deviceList).downloadKeys.mockResolvedValue({ mocked(client.crypto.deviceList).downloadKeys.mockResolvedValue(
"@alice:example.org": { aliceWeb, aliceMobile }, new Map([
"@bob:example.org": { bobDesktop }, [
}); "@alice:example.org",
new Map([
["aliceWeb", aliceWeb],
["aliceMobile", aliceMobile],
]),
],
["@bob:example.org", new Map([["bobDesktop", bobDesktop]])],
]),
);
await driver.sendToDevice("org.example.foo", true, contentMap); await driver.sendToDevice("org.example.foo", true, contentMap);
expect(client.encryptAndSendToDevices.mock.calls).toMatchSnapshot(); expect(client.encryptAndSendToDevices.mock.calls).toMatchSnapshot();