From 325cca670cda4d2ba45ccccef61e147917e39616 Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Fri, 20 Jan 2023 12:30:52 +0000
Subject: [PATCH 01/28] Upgrade matrix-js-sdk to 23.1.1
---
package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index f9e9a872554..fb604404cc2 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
- "matrix-js-sdk": "23.1.0",
+ "matrix-js-sdk": "23.1.1",
"matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
diff --git a/yarn.lock b/yarn.lock
index 58d4d7f3f9d..fd0801eb0f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6400,10 +6400,10 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
-matrix-js-sdk@23.1.0:
- version "23.1.0"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.1.0.tgz#c8dc89f82c5dce1ec25eedf05613cd9665cc1bbd"
- integrity sha512-/9eFWJBQceQX+2dHYZ5/9p0kjJs1PVDbIEWvR6FPjdnuNK3R4qi2NwpM6MoAHt8m7mX5qUG7x0NUWlalaOwrGQ==
+matrix-js-sdk@23.1.1:
+ version "23.1.1"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.1.1.tgz#0678edbf4d32241d7e3ef841597302559ac92608"
+ integrity sha512-lPxthMS2Cjd1jaQtYDRhfa2OwcIYS95UtrIwOCSFAI1Irh+PySgNenLjDLFdnmkwGmTf5LNrs5HKO1ZdcPY6pw==
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"
From dd34fe74e0e40169b2dfd25b5e8137da32824ed1 Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Fri, 20 Jan 2023 12:34:01 +0000
Subject: [PATCH 02/28] Prepare changelog for v3.64.2
---
CHANGELOG.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6a0f92e41a..495f538aef6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+Changes in [3.64.2](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.2) (2023-01-20)
+=====================================================================================================
+
+## 🐛 Bug Fixes
+ * Fix second occurence of a crash in older browsers
+
Changes in [3.64.1](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.1) (2023-01-18)
=====================================================================================================
From e3692ce2e45b4dedb2bfeb7355ea312928310748 Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Fri, 20 Jan 2023 12:34:04 +0000
Subject: [PATCH 03/28] v3.64.2
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index fb604404cc2..09682dc2922 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.64.1",
+ "version": "3.64.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
From 45fc0c05ca9ce3807830b38b19f506445dcb10bd Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Tue, 24 Jan 2023 11:27:41 +0000
Subject: [PATCH 04/28] Upgrade matrix-js-sdk to 23.2.0-rc.1
---
package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 17d2c761ffb..4a89679c55b 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
- "matrix-js-sdk": "23.1.1",
+ "matrix-js-sdk": "23.2.0-rc.1",
"matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
diff --git a/yarn.lock b/yarn.lock
index e65382d1bc9..718a5f8a4f8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6491,10 +6491,10 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
-matrix-js-sdk@23.1.1:
- version "23.1.1"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.1.1.tgz#0678edbf4d32241d7e3ef841597302559ac92608"
- integrity sha512-lPxthMS2Cjd1jaQtYDRhfa2OwcIYS95UtrIwOCSFAI1Irh+PySgNenLjDLFdnmkwGmTf5LNrs5HKO1ZdcPY6pw==
+matrix-js-sdk@23.2.0-rc.1:
+ version "23.2.0-rc.1"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.2.0-rc.1.tgz#9c908e9856e9d51b376cc57839f630ff0cf87291"
+ integrity sha512-PWj5DzQJzbAMsHMbOITvkKl/vn5+AS2k19uDhUNObpZJFpbOD+JV3tVEiTZlUxYhNzvtYDFG9Aj3T0Wokx5lqw==
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"
From d6161de463e8c0dbb22eeccaa0f766e8c2a405da Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Tue, 24 Jan 2023 11:29:45 +0000
Subject: [PATCH 05/28] Prepare changelog for v3.65.0-rc.1
---
CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 495f538aef6..ee73064d697 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,42 @@
+Changes in [3.65.0-rc.1](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.65.0-rc.1) (2023-01-24)
+===============================================================================================================
+
+## ✨ Features
+ * Quotes for rte ([\#9932](/~https://github.com/matrix-org/matrix-react-sdk/pull/9932)). Contributed by @alunturner.
+ * Show the room name in the room header during calls ([\#9942](/~https://github.com/matrix-org/matrix-react-sdk/pull/9942)). Fixes vector-im/element-web#24268.
+ * Add code blocks to rich text editor ([\#9921](/~https://github.com/matrix-org/matrix-react-sdk/pull/9921)). Contributed by @alunturner.
+ * Add new style for inline code ([\#9936](/~https://github.com/matrix-org/matrix-react-sdk/pull/9936)). Contributed by @florianduros.
+ * Add disabled button state to rich text editor ([\#9930](/~https://github.com/matrix-org/matrix-react-sdk/pull/9930)). Contributed by @alunturner.
+ * Change the rageshake "app" for auto-rageshakes ([\#9909](/~https://github.com/matrix-org/matrix-react-sdk/pull/9909)).
+ * Device manager - tweak settings display ([\#9905](/~https://github.com/matrix-org/matrix-react-sdk/pull/9905)). Contributed by @kerryarchibald.
+ * Add list functionality to rich text editor ([\#9871](/~https://github.com/matrix-org/matrix-react-sdk/pull/9871)). Contributed by @alunturner.
+
+## 🐛 Bug Fixes
+ * Fix RTE focus behaviour in threads ([\#9969](/~https://github.com/matrix-org/matrix-react-sdk/pull/9969)). Fixes vector-im/element-web#23755. Contributed by @florianduros.
+ * #22204 Issue: Centered File info in lightbox ([\#9971](/~https://github.com/matrix-org/matrix-react-sdk/pull/9971)). Fixes vector-im/element-web#22204. Contributed by @Spartan09.
+ * Fix seekbar position for zero length audio ([\#9949](/~https://github.com/matrix-org/matrix-react-sdk/pull/9949)). Fixes vector-im/element-web#24248.
+ * Allow thread panel to be closed after being opened from notification ([\#9937](/~https://github.com/matrix-org/matrix-react-sdk/pull/9937)). Fixes vector-im/element-web#23764 vector-im/element-web#23852 and vector-im/element-web#24213. Contributed by @justjanne.
+ * Only highlight focused menu item if focus is supposed to be visible ([\#9945](/~https://github.com/matrix-org/matrix-react-sdk/pull/9945)). Fixes vector-im/element-web#23582.
+ * Prevent call durations from breaking onto multiple lines ([\#9944](/~https://github.com/matrix-org/matrix-react-sdk/pull/9944)).
+ * Tweak call lobby buttons to more closely match designs ([\#9943](/~https://github.com/matrix-org/matrix-react-sdk/pull/9943)).
+ * Do not show a broadcast as live immediately after the recording has stopped ([\#9947](/~https://github.com/matrix-org/matrix-react-sdk/pull/9947)). Fixes vector-im/element-web#24233.
+ * Clear the RTE before sending a message ([\#9948](/~https://github.com/matrix-org/matrix-react-sdk/pull/9948)). Contributed by @florianduros.
+ * Fix {enter} press in RTE ([\#9927](/~https://github.com/matrix-org/matrix-react-sdk/pull/9927)). Contributed by @florianduros.
+ * Fix the problem that the password reset email has to be confirmed twice ([\#9926](/~https://github.com/matrix-org/matrix-react-sdk/pull/9926)). Fixes vector-im/element-web#24226.
+ * replace .at() with array.length-1 ([\#9933](/~https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281.
+ * Fix broken threads list timestamp layout ([\#9922](/~https://github.com/matrix-org/matrix-react-sdk/pull/9922)). Fixes vector-im/element-web#24243 and vector-im/element-web#24191. Contributed by @justjanne.
+ * Disable multiple messages when {enter} is pressed multiple times ([\#9929](/~https://github.com/matrix-org/matrix-react-sdk/pull/9929)). Fixes vector-im/element-web#24249. Contributed by @florianduros.
+ * Fix logout devices when resetting the password ([\#9925](/~https://github.com/matrix-org/matrix-react-sdk/pull/9925)). Fixes vector-im/element-web#24228.
+ * Fix: Poll replies overflow when not enough space ([\#9924](/~https://github.com/matrix-org/matrix-react-sdk/pull/9924)). Fixes vector-im/element-web#24227. Contributed by @kerryarchibald.
+ * State event updates are not forwarded to the widget from invitation room ([\#9802](/~https://github.com/matrix-org/matrix-react-sdk/pull/9802)). Contributed by @maheichyk.
+ * Fix error when viewing source of redacted events ([\#9914](/~https://github.com/matrix-org/matrix-react-sdk/pull/9914)). Fixes vector-im/element-web#24165. Contributed by @clarkf.
+ * Replace outdated css attribute ([\#9912](/~https://github.com/matrix-org/matrix-react-sdk/pull/9912)). Fixes vector-im/element-web#24218. Contributed by @justjanne.
+ * Clear isLogin theme override when user is no longer viewing login screens ([\#9911](/~https://github.com/matrix-org/matrix-react-sdk/pull/9911)). Fixes vector-im/element-web#23893.
+ * Fix reply action in message context menu notif & file panels ([\#9895](/~https://github.com/matrix-org/matrix-react-sdk/pull/9895)). Fixes vector-im/element-web#23970.
+ * Fix issue where thread dropdown would not show up correctly ([\#9872](/~https://github.com/matrix-org/matrix-react-sdk/pull/9872)). Fixes vector-im/element-web#24040. Contributed by @justjanne.
+ * Fix unexpected composer growing ([\#9889](/~https://github.com/matrix-org/matrix-react-sdk/pull/9889)). Contributed by @florianduros.
+ * Fix misaligned timestamps for thread roots which are emotes ([\#9875](/~https://github.com/matrix-org/matrix-react-sdk/pull/9875)). Fixes vector-im/element-web#23897. Contributed by @justjanne.
+
Changes in [3.64.2](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.2) (2023-01-20)
=====================================================================================================
From 403cc940f2c8be89a84865b40b2f15edf7488824 Mon Sep 17 00:00:00 2001
From: RiotRobot
Date: Tue, 24 Jan 2023 11:29:47 +0000
Subject: [PATCH 06/28] v3.65.0-rc.1
---
package.json | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 4a89679c55b..36f3ba49f85 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.64.2",
+ "version": "3.65.0-rc.1",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -23,7 +23,7 @@
"package.json",
".stylelintrc.js"
],
- "main": "./src/index.ts",
+ "main": "./lib/index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_lib_main": "./lib/index.ts",
"matrix_lib_typings": "./lib/index.d.ts",
@@ -259,5 +259,6 @@
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
- }
+ },
+ "typings": "./lib/index.d.ts"
}
From d84509d8d335d96cb865773f45a29924c923fccf Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 14:07:05 +0000
Subject: [PATCH 07/28] Implement MSC3946 for AdvancedRoomSettingsTab (#9995)
---
.../tabs/room/AdvancedRoomSettingsTab.tsx | 20 +++++----
src/i18n/strings/en_EN.json | 2 +
src/settings/Settings.tsx | 9 ++++
.../room/AdvancedRoomSettingsTab-test.tsx | 42 +++++++++++++++++++
4 files changed, 64 insertions(+), 9 deletions(-)
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index 809da069baf..d9acba8524a 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -19,13 +19,14 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
-import AccessibleButton from "../../../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import CopyableText from "../../../elements/CopyableText";
import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload";
+import SettingsStore from "../../../../../settings/SettingsStore";
interface IProps {
roomId: string;
@@ -46,9 +47,11 @@ interface IState {
}
export default class AdvancedRoomSettingsTab extends React.Component {
- public constructor(props, context) {
+ public constructor(props: IProps, context: any) {
super(props, context);
+ const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
+
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
@@ -60,11 +63,10 @@ export default class AdvancedRoomSettingsTab extends React.Component = {};
- const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
- const predecessor = createEvent ? createEvent.getContent().predecessor : null;
- if (predecessor && predecessor.room_id) {
- additionalStateChanges.oldRoomId = predecessor.room_id;
- additionalStateChanges.oldEventId = predecessor.event_id;
+ const predecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor);
+ if (predecessor) {
+ additionalStateChanges.oldRoomId = predecessor.roomId;
+ additionalStateChanges.oldEventId = predecessor.eventId;
}
this.setState({
@@ -75,12 +77,12 @@ export default class AdvancedRoomSettingsTab extends React.Component {
+ private upgradeRoom = (): void => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
Modal.createDialog(RoomUpgradeDialog, { room });
};
- private onOldRoomClicked = (e): void => {
+ private onOldRoomClicked = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 45db7cb23f2..6d315bf38d2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -955,6 +955,8 @@
"New group call experience": "New group call experience",
"Live Location Sharing": "Live Location Sharing",
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
+ "Dynamic room predecessors": "Dynamic room predecessors",
+ "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)",
"Favourite Messages": "Favourite Messages",
"Under active development.": "Under active development.",
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index bfd40e96ce1..fdd3a857c12 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -439,6 +439,15 @@ export const SETTINGS: { [setting: string]: ISetting } = {
shouldWarn: true,
default: false,
},
+ "feature_dynamic_room_predecessors": {
+ isFeature: true,
+ labsGroup: LabGroup.Rooms,
+ supportedLevels: LEVELS_FEATURE,
+ displayName: _td("Dynamic room predecessors"),
+ description: _td("Enable MSC3946 (to support late-arriving room archives)"),
+ shouldWarn: true,
+ default: false,
+ },
"feature_favourite_messages": {
isFeature: true,
labsGroup: LabGroup.Messaging,
diff --git a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
index dafd555966e..cb4b373e4fb 100644
--- a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
+++ b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
@@ -26,6 +26,7 @@ import { mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils";
import dis from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+import SettingsStore from "../../../../../../src/settings/SettingsStore";
jest.mock("../../../../../../src/dispatcher/dispatcher");
@@ -43,6 +44,12 @@ describe("AdvancedRoomSettingsTab", () => {
cli = MatrixClientPeg.get();
room = mkStubRoom(roomId, "test room", cli);
mocked(cli.getRoom).mockReturnValue(room);
+ mocked(dis.dispatch).mockReset();
+ mocked(room.findPredecessor).mockImplementation((msc3946: boolean) =>
+ msc3946
+ ? { roomId: "old_room_id_via_predecessor", eventId: null }
+ : { roomId: "old_room_id", eventId: "tombstone_event_id" },
+ );
});
it("should render as expected", () => {
@@ -71,6 +78,17 @@ describe("AdvancedRoomSettingsTab", () => {
room: room.roomId,
});
+ // Because we're mocking Room.findPredecessor, it may not be necessary
+ // to provide the actual event here, but we do need the create event,
+ // and in future this may be needed, so included for symmetry.
+ const predecessorEvent = mkEvent({
+ event: true,
+ user: "@a:b.com",
+ type: EventType.RoomPredecessor,
+ content: { predecessor_room_id: "old_room_id_via_predecessor" },
+ room: room.roomId,
+ });
+
type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null;
const getStateEvents = jest.spyOn(
@@ -82,6 +100,8 @@ describe("AdvancedRoomSettingsTab", () => {
switch (eventType) {
case EventType.RoomCreate:
return createEvent;
+ case EventType.RoomPredecessor:
+ return predecessorEvent;
default:
return null;
}
@@ -101,4 +121,26 @@ describe("AdvancedRoomSettingsTab", () => {
metricsViaKeyboard: false,
});
});
+
+ describe("When MSC3946 support is enabled", () => {
+ beforeEach(() => {
+ jest.spyOn(SettingsStore, "getValue")
+ .mockReset()
+ .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors");
+ });
+
+ it("should link to predecessor room via MSC3946 if enabled", async () => {
+ mockStateEvents(room);
+ const tab = renderTab();
+ const link = await tab.findByText("View older messages in test room.");
+ fireEvent.click(link);
+ expect(dis.dispatch).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ event_id: null,
+ room_id: "old_room_id_via_predecessor",
+ metricsTrigger: "WebPredecessorSettings",
+ metricsViaKeyboard: false,
+ });
+ });
+ });
});
From 364c453907afa169c54ec9c8e3bfc42d07f71406 Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 15:23:23 +0000
Subject: [PATCH 08/28] Tests for RoomCreate (#9997)
* Tests for RoomCreate tile
* Prefer screen instead of holding the return from render
* use userEvent instead of fireEvent
---
src/components/views/messages/RoomCreate.tsx | 6 +-
.../views/messages/RoomCreate-test.tsx | 87 +++++++++++++++++++
.../__snapshots__/RoomCreate-test.tsx.snap | 24 +++++
3 files changed, 116 insertions(+), 1 deletion(-)
create mode 100644 test/components/views/messages/RoomCreate-test.tsx
create mode 100644 test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx
index a9035ca03cb..8bff5dfdcc5 100644
--- a/src/components/views/messages/RoomCreate.tsx
+++ b/src/components/views/messages/RoomCreate.tsx
@@ -27,11 +27,15 @@ import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
- /* the MatrixEvent to show */
+ /** The m.room.create MatrixEvent that this tile represents */
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
}
+/**
+ * A message tile showing that this room was created as an upgrade of a previous
+ * room.
+ */
export default class RoomCreate extends React.Component {
private onLinkClicked = (e: React.MouseEvent): void => {
e.preventDefault();
diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx
new file mode 100644
index 00000000000..31763f9ae89
--- /dev/null
+++ b/test/components/views/messages/RoomCreate-test.tsx
@@ -0,0 +1,87 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { act, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { mocked } from "jest-mock";
+import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
+
+import dis from "../../../../src/dispatcher/dispatcher";
+import SettingsStore from "../../../../src/settings/SettingsStore";
+import RoomCreate from "../../../../src/components/views/messages/RoomCreate";
+import { stubClient } from "../../../test-utils/test-utils";
+import { Action } from "../../../../src/dispatcher/actions";
+
+jest.mock("../../../../src/dispatcher/dispatcher");
+
+describe("", () => {
+ const userId = "@alice:server.org";
+ const roomId = "!room:server.org";
+ const createEvent = new MatrixEvent({
+ type: EventType.RoomCreate,
+ sender: userId,
+ room_id: roomId,
+ content: {
+ predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" },
+ },
+ event_id: "$create",
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mocked(dis.dispatch).mockReset();
+ jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
+ jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
+ stubClient();
+ });
+
+ afterAll(() => {
+ jest.spyOn(SettingsStore, "getValue").mockRestore();
+ jest.spyOn(SettingsStore, "setValue").mockRestore();
+ });
+
+ it("Renders as expected", () => {
+ const roomCreate = render();
+ expect(roomCreate.asFragment()).toMatchSnapshot();
+ });
+
+ it("Links to the old version of the room", () => {
+ render();
+ expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
+ "href",
+ "https://matrix.to/#/old_room_id/tombstone_event_id",
+ );
+ });
+
+ it("Opens the old room on click", async () => {
+ render();
+ const link = screen.getByText("Click here to see older messages.");
+
+ await act(() => userEvent.click(link));
+
+ await waitFor(() =>
+ expect(dis.dispatch).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ event_id: "tombstone_event_id",
+ highlighted: true,
+ room_id: "old_room_id",
+ metricsTrigger: "Predecessor",
+ metricsViaKeyboard: false,
+ }),
+ );
+ });
+});
diff --git a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
new file mode 100644
index 00000000000..97c1cee66f6
--- /dev/null
+++ b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` Renders as expected 1`] = `
+
+
+
+ This room is a continuation of another conversation.
+
+
+
+
+`;
From 64cec319812e9a72c386c1643d20db17d1bbe480 Mon Sep 17 00:00:00 2001
From: Nawaraj Shah
Date: Sun, 29 Jan 2023 15:48:38 +0545
Subject: [PATCH 09/28] changing the color of message time stamp (#10016)
---
res/css/views/right_panel/_ThreadPanel.pcss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss
index aff186fca5d..c1f1daaca1d 100644
--- a/res/css/views/right_panel/_ThreadPanel.pcss
+++ b/res/css/views/right_panel/_ThreadPanel.pcss
@@ -130,7 +130,7 @@ limitations under the License.
}
.mx_MessageTimestamp {
- color: $secondary-content;
+ color: $event-timestamp-color;
}
.mx_BaseCard_footer {
From a8aa4de4b4c14a6a995071aab3f2f871b3ff2388 Mon Sep 17 00:00:00 2001
From: Clark Fischer <439978+clarkf@users.noreply.github.com>
Date: Mon, 30 Jan 2023 09:50:08 +0000
Subject: [PATCH 10/28] Member avatars without canvas (#9990)
* Strict typechecking fixes for Base/Member/Avatar
Update the core avatar files to pass `--strict --noImplicitAny` typechecks.
Signed-off-by: Clark Fischer
* Add tests for Base/Member/Avatar
More thoroughly test the core avatar files. Not necessarily the most thorough,
but an improvement.
Signed-off-by: Clark Fischer
* Extract TextAvatar from BaseAvatar
Extracted the fallback/textual avatar into its own component.
Signed-off-by: Clark Fischer
* Use standard HTML for non-image avatars
Firefox users with `resistFingerprinting` enabled were seeing random noise
for rooms and users without avatars. There's no real reason to use data
URLs to present flat colors.
This converts non-image avatars to inline blocks with background colors.
See /~https://github.com/vector-im/element-web/issues/23936
Signed-off-by: Clark Fischer
* Have pills use solid backgrounds rather than colored images
Similar to room and member avatars, pills now use colored pseudo-elements
rather than background images.
Signed-off-by: Clark Fischer
---------
Signed-off-by: Clark Fischer
Co-authored-by: Andy Balaam
---
.../views/rooms/_BasicMessageComposer.pcss | 2 +-
src/Avatar.ts | 64 ++++--
src/components/views/avatars/BaseAvatar.tsx | 128 ++++++-----
src/components/views/avatars/MemberAvatar.tsx | 22 +-
src/components/views/avatars/RoomAvatar.tsx | 3 +-
src/dispatcher/payloads/ViewUserPayload.ts | 7 +
src/editor/parts.ts | 40 ++--
test/Avatar-test.ts | 121 ++++++++---
.../__snapshots__/RoomView-test.tsx.snap | 98 +++------
.../__snapshots__/UserMenu-test.tsx.snap | 14 +-
.../views/avatars/BaseAvatar-test.tsx | 201 ++++++++++++++++++
.../views/avatars/MemberAvatar-test.tsx | 94 ++++++--
.../views/avatars/RoomAvatar-test.tsx | 10 +-
.../__snapshots__/BaseAvatar-test.tsx.snap | 72 +++++++
.../__snapshots__/MemberAvatar-test.tsx.snap | 14 ++
.../__snapshots__/RoomAvatar-test.tsx.snap | 42 ++--
.../__snapshots__/BeaconMarker-test.tsx.snap | 16 +-
.../views/rooms/RoomHeader-test.tsx | 10 +-
.../RoomPreviewBar-test.tsx.snap | 28 +--
.../__snapshots__/RoomTile-test.tsx.snap | 14 +-
test/editor/__snapshots__/parts-test.ts.snap | 45 ++++
test/editor/parts-test.ts | 72 ++++++-
22 files changed, 806 insertions(+), 311 deletions(-)
create mode 100644 test/components/views/avatars/BaseAvatar-test.tsx
create mode 100644 test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
create mode 100644 test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
create mode 100644 test/editor/__snapshots__/parts-test.ts.snap
diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss
index 7b88a058153..32e7c5288f6 100644
--- a/res/css/views/rooms/_BasicMessageComposer.pcss
+++ b/res/css/views/rooms/_BasicMessageComposer.pcss
@@ -78,7 +78,7 @@ limitations under the License.
min-width: $font-16px; /* ensure the avatar is not compressed */
height: $font-16px;
margin-inline-end: 0.24rem;
- background: var(--avatar-background), $background;
+ background: var(--avatar-background);
color: $avatar-initial-color;
background-repeat: no-repeat;
background-size: $font-16px;
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 8a3f10a22ca..3e6b18dbc79 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015, 2016, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,16 +24,19 @@ import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
+const DEFAULT_COLORS: Readonly = ["#0DBD8B", "#368bd6", "#ac3ba8"];
+
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
- member: RoomMember,
+ member: RoomMember | null | undefined,
width: number,
height: number,
resizeMethod: ResizeMethod,
): string {
- let url: string;
- if (member?.getMxcAvatarUrl()) {
- url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ let url: string | undefined;
+ const mxcUrl = member?.getMxcAvatarUrl();
+ if (mxcUrl) {
+ url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
if (!url) {
// member can be null here currently since on invites, the JS SDK
@@ -44,6 +47,17 @@ export function avatarUrlForMember(
return url;
}
+export function getMemberAvatar(
+ member: RoomMember | null | undefined,
+ width: number,
+ height: number,
+ resizeMethod: ResizeMethod,
+): string | undefined {
+ const mxcUrl = member?.getMxcAvatarUrl();
+ if (!mxcUrl) return undefined;
+ return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
+}
+
export function avatarUrlForUser(
user: Pick,
width: number,
@@ -86,18 +100,10 @@ function urlForColor(color: string): string {
// hard to install a listener here, even if there were a clear event to listen to
const colorToDataURLCache = new Map();
-export function defaultAvatarUrlForString(s: string): string {
+export function defaultAvatarUrlForString(s: string | undefined): string {
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
- const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
- let total = 0;
- for (let i = 0; i < s.length; ++i) {
- total += s.charCodeAt(i);
- }
- const colorIndex = total % defaultColors.length;
- // overwritten color value in custom themes
- const cssVariable = `--avatar-background-colors_${colorIndex}`;
- const cssValue = document.body.style.getPropertyValue(cssVariable);
- const color = cssValue || defaultColors[colorIndex];
+
+ const color = getColorForString(s);
let dataUrl = colorToDataURLCache.get(color);
if (!dataUrl) {
// validate color as this can come from account_data
@@ -112,13 +118,23 @@ export function defaultAvatarUrlForString(s: string): string {
return dataUrl;
}
+export function getColorForString(input: string): string {
+ const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0);
+ const index = charSum % DEFAULT_COLORS.length;
+
+ // overwritten color value in custom themes
+ const cssVariable = `--avatar-background-colors_${index}`;
+ const cssValue = document.body.style.getPropertyValue(cssVariable);
+ return cssValue || DEFAULT_COLORS[index]!;
+}
+
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
-export function getInitialLetter(name: string): string {
+export function getInitialLetter(name: string): string | undefined {
if (!name) {
// XXX: We should find out what causes the name to sometimes be falsy.
console.trace("`name` argument to `getInitialLetter` not supplied");
@@ -134,19 +150,20 @@ export function getInitialLetter(name: string): string {
}
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
- return split(name, "", 1)[0].toUpperCase();
+ return split(name, "", 1)[0]!.toUpperCase();
}
export function avatarUrlForRoom(
- room: Room,
+ room: Room | undefined,
width: number,
height: number,
resizeMethod?: ResizeMethod,
): string | null {
if (!room) return null; // null-guard
- if (room.getMxcAvatarUrl()) {
- return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ const mxcUrl = room.getMxcAvatarUrl();
+ if (mxcUrl) {
+ return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
// space rooms cannot be DMs so skip the rest
@@ -159,8 +176,9 @@ export function avatarUrlForRoom(
// If there are only two members in the DM use the avatar of the other member
const otherMember = room.getAvatarFallbackMember();
- if (otherMember?.getMxcAvatarUrl()) {
- return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ const otherMemberMxc = otherMember?.getMxcAvatarUrl();
+ if (otherMemberMxc) {
+ return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
return null;
}
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 025cb9d2711..d1dbe7743de 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -1,8 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2018, 2019, 2020, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,34 +19,41 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { ClientEvent } from "matrix-js-sdk/src/client";
+import { SyncState } from "matrix-js-sdk/src/sync";
import * as AvatarLogic from "../../../Avatar";
-import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { toPx } from "../../../utils/units";
import { _t } from "../../../languageHandler";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
- name: string; // The name (first initial used as default)
- idName?: string; // ID for generating hash colours
- title?: string; // onHover title text
- url?: string; // highest priority of them all, shortcut to set in urls[0]
- urls?: string[]; // [highest_priority, ... , lowest_priority]
+ /** The name (first initial used as default) */
+ name: string;
+ /** ID for generating hash colours */
+ idName?: string;
+ /** onHover title text */
+ title?: string;
+ /** highest priority of them all, shortcut to set in urls[0] */
+ url?: string;
+ /** [highest_priority, ... , lowest_priority] */
+ urls?: string[];
width?: number;
height?: number;
- // XXX: resizeMethod not actually used.
+ /** @deprecated not actually used */
resizeMethod?: ResizeMethod;
- defaultToInitialLetter?: boolean; // true to add default url
- onClick?: React.MouseEventHandler;
+ /** true to add default url */
+ defaultToInitialLetter?: boolean;
+ onClick?: React.ComponentPropsWithoutRef["onClick"];
inputRef?: React.RefObject;
className?: string;
tabIndex?: number;
}
-const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => {
+const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
@@ -66,11 +71,26 @@ const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): stri
return Array.from(new Set(_urls));
};
-const useImageUrl = ({ url, urls }): [string, () => void] => {
+/**
+ * Hook for cycling through a changing set of images.
+ *
+ * The set of images is updated whenever `url` or `urls` change, the user's
+ * `lowBandwidth` preference changes, or the client reconnects.
+ *
+ * Returns `[imageUrl, onError]`. When `onError` is called, the next image in
+ * the set will be displayed.
+ */
+const useImageUrl = ({
+ url,
+ urls,
+}: {
+ url: string | undefined;
+ urls: string[] | undefined;
+}): [string | undefined, () => void] => {
// Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);
- const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
+ const lowBandwidth = roomContext.lowBandwidth;
const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState(0);
@@ -85,10 +105,10 @@ const useImageUrl = ({ url, urls }): [string, () => void] => {
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
- const onClientSync = useCallback((syncState, prevState) => {
+ const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
- const reconnected = syncState !== "ERROR" && prevState !== syncState;
+ const reconnected = syncState !== SyncState.Error && prevState !== syncState;
if (reconnected) {
setIndex(0);
}
@@ -108,46 +128,18 @@ const BaseAvatar: React.FC = (props) => {
urls,
width = 40,
height = 40,
- resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true,
onClick,
inputRef,
className,
+ resizeMethod: _unused, // to keep it from being in `otherProps`
...otherProps
} = props;
const [imageUrl, onError] = useImageUrl({ url, urls });
if (!imageUrl && defaultToInitialLetter && name) {
- const initialLetter = AvatarLogic.getInitialLetter(name);
- const textNode = (
-
- {initialLetter}
-
- );
- const imgNode = (
-
- );
+ const avatar = ;
if (onClick) {
return (
@@ -159,9 +151,12 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
onClick={onClick}
inputRef={inputRef}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
>
- {textNode}
- {imgNode}
+ {avatar}
);
} else {
@@ -170,10 +165,13 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
ref={inputRef}
{...otherProps}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
role="presentation"
>
- {textNode}
- {imgNode}
+ {avatar}
);
}
@@ -220,3 +218,31 @@ const BaseAvatar: React.FC = (props) => {
export default BaseAvatar;
export type BaseAvatarType = React.FC;
+
+const TextAvatar: React.FC<{
+ name: string;
+ idName?: string;
+ width: number;
+ height: number;
+ title?: string;
+}> = ({ name, idName, width, height, title }) => {
+ const initialLetter = AvatarLogic.getInitialLetter(name);
+
+ return (
+
+ {initialLetter}
+
+ );
+};
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 48138714559..f493c58f8cf 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -1,6 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2019 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,6 +25,7 @@ import { mediaFromMxc } from "../../../customisations/Media";
import { CardContext } from "../right_panel/context";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
+import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
interface IProps extends Omit, "name" | "idName" | "url"> {
member: RoomMember | null;
@@ -33,14 +33,13 @@ interface IProps extends Omit, "name" |
width: number;
height: number;
resizeMethod?: ResizeMethod;
- // The onClick to give the avatar
- onClick?: React.MouseEventHandler;
- // Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
+ /** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */
viewUserOnClick?: boolean;
pushUserOnClick?: boolean;
title?: string;
- style?: any;
- forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
+ style?: React.CSSProperties;
+ /** true to deny `useOnlyCurrentProfiles` usage. Default false. */
+ forceHistorical?: boolean;
hideTitle?: boolean;
}
@@ -77,8 +76,8 @@ export default function MemberAvatar({
if (!title) {
title =
- UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", {
- roomId: member?.roomId ?? "",
+ UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, {
+ roomId: member.roomId,
}) ?? fallbackUserId;
}
}
@@ -88,7 +87,6 @@ export default function MemberAvatar({
{...props}
width={width}
height={height}
- resizeMethod={resizeMethod}
name={name ?? ""}
title={hideTitle ? undefined : title}
idName={member?.userId ?? fallbackUserId}
@@ -96,9 +94,9 @@ export default function MemberAvatar({
onClick={
viewUserOnClick
? () => {
- dis.dispatch({
+ dis.dispatch({
action: Action.ViewUser,
- member: propsMember,
+ member: propsMember || undefined,
push: card.isCard,
});
}
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 50389c77491..4abfdbbf67e 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -109,7 +109,8 @@ export default class RoomAvatar extends React.Component {
}
private onRoomAvatarClick = (): void => {
- const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
+ const avatarMxc = this.props.room?.getMxcAvatarUrl();
+ const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null;
const params = {
src: avatarUrl,
name: this.props.room.name,
diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts
index 20df21beb49..a09804babee 100644
--- a/src/dispatcher/payloads/ViewUserPayload.ts
+++ b/src/dispatcher/payloads/ViewUserPayload.ts
@@ -28,4 +28,11 @@ export interface ViewUserPayload extends ActionPayload {
* should be shown (hide whichever relevant components).
*/
member?: RoomMember | User;
+
+ /**
+ * Should this event be pushed as a card into the right panel?
+ *
+ * @see RightPanelStore#pushCard
+ */
+ push?: boolean;
}
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 306d86dbc94..0157cd738a0 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -295,9 +294,9 @@ export abstract class PillPart extends BasePart implements IPillPart {
}
// helper method for subclasses
- protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void {
- const avatarBackground = `url('${avatarUrl}')`;
- const avatarLetter = `'${initialLetter}'`;
+ protected setAvatarVars(node: HTMLElement, avatarBackground: string, initialLetter: string | undefined): void {
+ // const avatarBackground = `url('${avatarUrl}')`;
+ const avatarLetter = `'${initialLetter || ""}'`;
// check if the value is changing,
// otherwise the avatars flicker on every keystroke while updating.
if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
@@ -413,13 +412,15 @@ class RoomPillPart extends PillPart {
}
protected setAvatar(node: HTMLElement): void {
- let initialLetter = "";
- let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
- if (!avatarUrl) {
- initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
- avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId);
+ const avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
+ if (avatarUrl) {
+ this.setAvatarVars(node, `url('${avatarUrl}')`, "");
+ return;
}
- this.setAvatarVars(node, avatarUrl, initialLetter);
+
+ const initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
+ const color = Avatar.getColorForString(this.room?.roomId ?? this.resourceId);
+ this.setAvatarVars(node, color, initialLetter);
}
public get type(): IPillPart["type"] {
@@ -465,14 +466,17 @@ class UserPillPart extends PillPart {
if (!this.member) {
return;
}
- const name = this.member.name || this.member.userId;
- const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
- const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
- let initialLetter = "";
- if (avatarUrl === defaultAvatarUrl) {
- initialLetter = Avatar.getInitialLetter(name);
+
+ const avatar = Avatar.getMemberAvatar(this.member, 16, 16, "crop");
+ if (avatar) {
+ this.setAvatarVars(node, `url('${avatar}')`, "");
+ return;
}
- this.setAvatarVars(node, avatarUrl, initialLetter);
+
+ const name = this.member.name || this.member.userId;
+ const initialLetter = Avatar.getInitialLetter(name);
+ const color = Avatar.getColorForString(this.member.userId);
+ this.setAvatarVars(node, color, initialLetter);
}
protected onClick = (): void => {
diff --git a/test/Avatar-test.ts b/test/Avatar-test.ts
index 0ff064ed57d..8b4ee03b7fc 100644
--- a/test/Avatar-test.ts
+++ b/test/Avatar-test.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,33 +15,106 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
-import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
-
-import { avatarUrlForRoom } from "../src/Avatar";
-import { Media, mediaFromMxc } from "../src/customisations/Media";
+import { Room, RoomMember, RoomType, User } from "matrix-js-sdk/src/matrix";
+
+import {
+ avatarUrlForMember,
+ avatarUrlForRoom,
+ avatarUrlForUser,
+ defaultAvatarUrlForString,
+ getColorForString,
+ getInitialLetter,
+} from "../src/Avatar";
+import { mediaFromMxc } from "../src/customisations/Media";
import DMRoomMap from "../src/utils/DMRoomMap";
-
-jest.mock("../src/customisations/Media", () => ({
- mediaFromMxc: jest.fn(),
-}));
+import { filterConsole, stubClient } from "./test-utils";
const roomId = "!room:example.com";
const avatarUrl1 = "https://example.com/avatar1";
const avatarUrl2 = "https://example.com/avatar2";
+describe("avatarUrlForMember", () => {
+ let member: RoomMember;
+
+ beforeEach(() => {
+ stubClient();
+ member = new RoomMember(roomId, "@user:example.com");
+ });
+
+ it("returns the member's url", () => {
+ const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxc);
+
+ expect(avatarUrlForMember(member, 32, 32, "crop")).toBe(
+ mediaFromMxc(mxc).getThumbnailOfSourceHttp(32, 32, "crop"),
+ );
+ });
+
+ it("returns a default if the member has no avatar", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
+
+ expect(avatarUrlForMember(member, 32, 32, "crop")).toMatch(/^data:/);
+ });
+});
+
+describe("avatarUrlForUser", () => {
+ let user: User;
+
+ beforeEach(() => {
+ stubClient();
+ user = new User("@user:example.com");
+ });
+
+ it("should return the user's avatar", () => {
+ const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
+ user.avatarUrl = mxc;
+
+ expect(avatarUrlForUser(user, 64, 64, "scale")).toBe(
+ mediaFromMxc(mxc).getThumbnailOfSourceHttp(64, 64, "scale"),
+ );
+ });
+
+ it("should not provide a fallback", () => {
+ expect(avatarUrlForUser(user, 64, 64, "scale")).toBeNull();
+ });
+});
+
+describe("defaultAvatarUrlForString", () => {
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(defaultAvatarUrlForString(s)).not.toBe("");
+ });
+});
+
+describe("getColorForString", () => {
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(getColorForString(s)).toMatch(/^#\w+$/);
+ });
+
+ it("should return different values for different strings", () => {
+ expect(getColorForString("a")).not.toBe(getColorForString("b"));
+ });
+});
+
+describe("getInitialLetter", () => {
+ filterConsole("argument to `getInitialLetter` not supplied");
+
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(getInitialLetter(s)).not.toBe("");
+ });
+
+ it("should return undefined for empty strings", () => {
+ expect(getInitialLetter("")).toBeUndefined();
+ });
+});
+
describe("avatarUrlForRoom", () => {
- let getThumbnailOfSourceHttp: jest.Mock;
let room: Room;
let roomMember: RoomMember;
let dmRoomMap: DMRoomMap;
beforeEach(() => {
- getThumbnailOfSourceHttp = jest.fn();
- mocked(mediaFromMxc).mockImplementation((): Media => {
- return {
- getThumbnailOfSourceHttp,
- } as unknown as Media;
- });
+ stubClient();
+
room = {
roomId,
getMxcAvatarUrl: jest.fn(),
@@ -59,14 +132,14 @@ describe("avatarUrlForRoom", () => {
});
it("should return null for a null room", () => {
- expect(avatarUrlForRoom(null, 128, 128)).toBeNull();
+ expect(avatarUrlForRoom(undefined, 128, 128)).toBeNull();
});
it("should return the HTTP source if the room provides a MXC url", () => {
mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1);
- getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
- expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
- expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
+ expect(avatarUrlForRoom(room, 128, 256, "crop")).toBe(
+ mediaFromMxc(avatarUrl1).getThumbnailOfSourceHttp(128, 256, "crop"),
+ );
});
it("should return null for a space room", () => {
@@ -83,7 +156,7 @@ describe("avatarUrlForRoom", () => {
it("should return null if there is no other member in the room", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
- mocked(room.getAvatarFallbackMember).mockReturnValue(null);
+ mocked(room.getAvatarFallbackMember).mockReturnValue(undefined);
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
});
@@ -97,8 +170,8 @@ describe("avatarUrlForRoom", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2);
- getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
- expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
- expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
+ expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(
+ mediaFromMxc(avatarUrl2).getThumbnailOfSourceHttp(128, 256, "crop"),
+ );
});
});
diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap
index 47318525d56..c81e180c421 100644
--- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap
+++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap
@@ -20,22 +20,16 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
U
-
@@ -119,22 +113,16 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
U
-
@@ -215,23 +203,17 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
@@ -314,22 +296,16 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
U
-
@@ -410,23 +386,17 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
@@ -581,22 +551,16 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
U
-
@@ -672,23 +636,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
diff --git a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
index 769711434a8..0546900abb3 100644
--- a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
+++ b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
@@ -20,22 +20,16 @@ exports[` when rendered should render as expected 1`] = `
diff --git a/test/components/views/avatars/BaseAvatar-test.tsx b/test/components/views/avatars/BaseAvatar-test.tsx
new file mode 100644
index 00000000000..294a64c4362
--- /dev/null
+++ b/test/components/views/avatars/BaseAvatar-test.tsx
@@ -0,0 +1,201 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { fireEvent, render } from "@testing-library/react";
+import { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import React from "react";
+import { act } from "react-dom/test-utils";
+import { SyncState } from "matrix-js-sdk/src/sync";
+
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import RoomContext from "../../../../src/contexts/RoomContext";
+import { getRoomContext } from "../../../test-utils/room";
+import { stubClient } from "../../../test-utils/test-utils";
+import BaseAvatar from "../../../../src/components/views/avatars/BaseAvatar";
+import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
+
+type Props = React.ComponentPropsWithoutRef;
+
+describe("", () => {
+ let client: MatrixClient;
+ let room: Room;
+ let member: RoomMember;
+
+ function getComponent(props: Partial) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ function failLoadingImg(container: HTMLElement): void {
+ const img = container.querySelector("img")!;
+ expect(img).not.toBeNull();
+ act(() => {
+ fireEvent.error(img);
+ });
+ }
+
+ function emitReconnect(): void {
+ act(() => {
+ client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Reconnecting);
+ });
+ }
+
+ beforeEach(() => {
+ client = stubClient();
+
+ room = new Room("!room:example.com", client, client.getUserId() ?? "", {
+ pendingEventOrdering: PendingEventOrdering.Detached,
+ });
+
+ member = new RoomMember(room.roomId, "@bob:example.org");
+ jest.spyOn(room, "getMember").mockReturnValue(member);
+ });
+
+ it("renders with minimal properties", () => {
+ const { container } = render(getComponent({}));
+
+ expect(container.querySelector(".mx_BaseAvatar")).not.toBeNull();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ const { container } = render(
+ getComponent({
+ name: "CoolUser22",
+ title: "Hover title",
+ url: "https://example.com/images/avatar.gif",
+ className: "mx_SomethingArbitrary",
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar + click)", () => {
+ const { container } = render(
+ getComponent({
+ name: "CoolUser22",
+ title: "Hover title",
+ url: "https://example.com/images/avatar.gif",
+ className: "mx_SomethingArbitrary",
+ onClick: () => {},
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ const { container } = render(
+ getComponent({
+ name: "xX_Element_User_Xx",
+ title: ":kiss:",
+ defaultToInitialLetter: true,
+ className: "big-and-bold",
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (no avatar + click)", () => {
+ const { container } = render(
+ getComponent({
+ name: "xX_Element_User_Xx",
+ title: ":kiss:",
+ defaultToInitialLetter: true,
+ className: "big-and-bold",
+ onClick: () => {},
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("uses fallback images", () => {
+ const images = [...Array(10)].map((_, i) => `https://example.com/images/${i}.webp`);
+
+ const { container } = render(
+ getComponent({
+ url: images[0],
+ urls: images.slice(1),
+ }),
+ );
+
+ for (const image of images) {
+ expect(container.querySelector("img")!.src).toBe(image);
+ failLoadingImg(container);
+ }
+ });
+
+ it("re-renders on reconnect", () => {
+ const primary = "https://example.com/image.jpeg";
+ const fallback = "https://example.com/fallback.png";
+ const { container } = render(
+ getComponent({
+ url: primary,
+ urls: [fallback],
+ }),
+ );
+
+ failLoadingImg(container);
+ expect(container.querySelector("img")!.src).toBe(fallback);
+
+ emitReconnect();
+ expect(container.querySelector("img")!.src).toBe(primary);
+ });
+
+ it("renders with an image", () => {
+ const url = "https://example.com/images/small/avatar.gif?size=realBig";
+ const { container } = render(getComponent({ url }));
+
+ const img = container.querySelector("img");
+ expect(img!.src).toBe(url);
+ });
+
+ it("renders the initial letter", () => {
+ const { container } = render(getComponent({ name: "Yellow", defaultToInitialLetter: true }));
+
+ const avatar = container.querySelector(".mx_BaseAvatar_initial")!;
+ expect(avatar.innerHTML).toBe("Y");
+ });
+
+ it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])(
+ "includes a click handler",
+ (props: Partial) => {
+ const onClick = jest.fn();
+
+ const { container } = render(
+ getComponent({
+ ...props,
+ onClick,
+ }),
+ );
+
+ act(() => {
+ fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
+ });
+
+ expect(onClick).toHaveBeenCalled();
+ },
+ );
+});
diff --git a/test/components/views/avatars/MemberAvatar-test.tsx b/test/components/views/avatars/MemberAvatar-test.tsx
index 4895b70f217..3dc793bd929 100644
--- a/test/components/views/avatars/MemberAvatar-test.tsx
+++ b/test/components/views/avatars/MemberAvatar-test.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,19 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { getByTestId, render, waitFor } from "@testing-library/react";
-import { mocked } from "jest-mock";
+import { fireEvent, getByTestId, render } from "@testing-library/react";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import React from "react";
+import { act } from "react-dom/test-utils";
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
import RoomContext from "../../../../src/contexts/RoomContext";
-import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
+import { mediaFromMxc } from "../../../../src/customisations/Media";
+import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload";
+import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
+import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext } from "../../../test-utils/room";
import { stubClient } from "../../../test-utils/test-utils";
+import { Action } from "../../../../src/dispatcher/actions";
+
+type Props = React.ComponentPropsWithoutRef;
describe("MemberAvatar", () => {
const ROOM_ID = "roomId";
@@ -35,7 +41,7 @@ describe("MemberAvatar", () => {
let room: Room;
let member: RoomMember;
- function getComponent(props) {
+ function getComponent(props: Partial) {
return (
@@ -44,10 +50,7 @@ describe("MemberAvatar", () => {
}
beforeEach(() => {
- jest.clearAllMocks();
-
- stubClient();
- mockClient = mocked(MatrixClientPeg.get());
+ mockClient = stubClient();
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
@@ -55,22 +58,77 @@ describe("MemberAvatar", () => {
member = new RoomMember(ROOM_ID, "@bob:example.org");
jest.spyOn(room, "getMember").mockReturnValue(member);
+ });
+
+ it("supports 'null' members", () => {
+ const { container } = render(getComponent({ member: null }));
+
+ expect(container.querySelector("img")).not.toBeNull();
+ });
+
+ it("matches the snapshot", () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
+ const { container } = render(
+ getComponent({
+ member,
+ fallbackUserId: "Fallback User ID",
+ title: "Hover title",
+ style: {
+ color: "pink",
+ },
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
});
- it("shows an avatar for useOnlyCurrentProfiles", async () => {
- jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
- return settingName === "useOnlyCurrentProfiles";
- });
+ it("shows an avatar for useOnlyCurrentProfiles", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
+
+ SettingsStore.setValue("useOnlyCurrentProfiles", null, SettingLevel.DEVICE, true);
const { container } = render(getComponent({}));
- let avatar: HTMLElement;
- await waitFor(() => {
- avatar = getByTestId(container, "avatar-img");
- expect(avatar).toBeInTheDocument();
+ const avatar = getByTestId(container, "avatar-img");
+ expect(avatar).toBeInTheDocument();
+ expect(avatar.getAttribute("src")).not.toBe("");
+ });
+
+ it("uses the member's configured avatar", () => {
+ const mxcUrl = "mxc://example.com/avatars/user.tiff";
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxcUrl);
+
+ const { container } = render(getComponent({ member }));
+
+ const img = container.querySelector("img");
+ expect(img).not.toBeNull();
+ expect(img!.src).toBe(mediaFromMxc(mxcUrl).srcHttp);
+ });
+
+ it("uses a fallback when the member has no avatar", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
+
+ const { container } = render(getComponent({ member }));
+
+ const img = container.querySelector(".mx_BaseAvatar_image");
+ expect(img).not.toBeNull();
+ });
+
+ it("dispatches on click", () => {
+ const { container } = render(getComponent({ member, viewUserOnClick: true }));
+
+ const spy = jest.spyOn(defaultDispatcher, "dispatch");
+
+ act(() => {
+ fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
});
- expect(avatar!.getAttribute("src")).not.toBe("");
+ expect(spy).toHaveBeenCalled();
+ const [payload] = spy.mock.lastCall!;
+ expect(payload).toStrictEqual({
+ action: Action.ViewUser,
+ member,
+ push: false,
+ });
});
});
diff --git a/test/components/views/avatars/RoomAvatar-test.tsx b/test/components/views/avatars/RoomAvatar-test.tsx
index e23cd96f02d..7be7dd65e90 100644
--- a/test/components/views/avatars/RoomAvatar-test.tsx
+++ b/test/components/views/avatars/RoomAvatar-test.tsx
@@ -39,7 +39,7 @@ describe("RoomAvatar", () => {
const dmRoomMap = new DMRoomMap(client);
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
- jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
+ jest.spyOn(AvatarModule, "getColorForString");
});
afterAll(() => {
@@ -48,14 +48,14 @@ describe("RoomAvatar", () => {
afterEach(() => {
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
- mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
+ mocked(AvatarModule.getColorForString).mockClear();
});
it("should render as expected for a Room", () => {
const room = new Room("!room:example.com", client, client.getSafeUserId());
room.name = "test room";
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(room.roomId);
});
it("should render as expected for a DM room", () => {
@@ -64,7 +64,7 @@ describe("RoomAvatar", () => {
room.name = "DM room";
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId);
});
it("should render as expected for a LocalRoom", () => {
@@ -73,6 +73,6 @@ describe("RoomAvatar", () => {
localRoom.name = "local test room";
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId);
});
});
diff --git a/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
new file mode 100644
index 00000000000..da62540b90e
--- /dev/null
+++ b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot (avatar + click) 1`] = `
+
+
+
+`;
+
+exports[` matches snapshot (avatar) 1`] = `
+
+
+
+`;
+
+exports[` matches snapshot (no avatar + click) 1`] = `
+
+
+
+ X
+
+
+
+`;
+
+exports[` matches snapshot (no avatar) 1`] = `
+
+
+
+ X
+
+
+
+`;
diff --git a/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
new file mode 100644
index 00000000000..feda79035cd
--- /dev/null
+++ b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MemberAvatar matches the snapshot 1`] = `
+
+
+
+`;
diff --git a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
index 6bffa157b63..699113689e4 100644
--- a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
+++ b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
@@ -5,22 +5,16 @@ exports[`RoomAvatar should render as expected for a DM room 1`] = `
D
-
`;
@@ -30,22 +24,16 @@ exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
L
-
`;
@@ -55,22 +43,16 @@ exports[`RoomAvatar should render as expected for a Room 1`] = `
T
-
`;
diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
index b42ccb83ee3..b965f50b2f9 100644
--- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
@@ -13,23 +13,17 @@ exports[` renders marker when beacon has location 1`] = `
A
-
diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx
index 5857e282957..c27d4c0c20f 100644
--- a/test/components/views/rooms/RoomHeader-test.tsx
+++ b/test/components/views/rooms/RoomHeader-test.tsx
@@ -72,7 +72,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a room with 2 people", () => {
@@ -86,7 +86,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a room with >2 people", () => {
@@ -100,7 +100,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a DM with only ourselves", () => {
@@ -114,7 +114,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the user avatar in a DM with 2 people", () => {
@@ -148,7 +148,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("renders call buttons normally", () => {
diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
index f35467e1efd..a78a452e890 100644
--- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
+++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
@@ -161,22 +161,16 @@ exports[` with an invite without an invited email for a dm roo
R
-
@@ -236,22 +230,16 @@ exports[` with an invite without an invited email for a non-dm
R
-
diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
index bcbb7932c69..557d97c243e 100644
--- a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
+++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
@@ -15,22 +15,16 @@ exports[`RoomTile should render the room 1`] = `
!
-
+ !room:example.com
+
+`;
+
+exports[`RoomPillPart matches snapshot (no avatar) 1`] = `
+
+ !room:example.com
+
+`;
+
+exports[`UserPillPart matches snapshot (avatar) 1`] = `
+
+ DisplayName
+
+`;
+
+exports[`UserPillPart matches snapshot (no avatar) 1`] = `
+
+ DisplayName
+
+`;
diff --git a/test/editor/parts-test.ts b/test/editor/parts-test.ts
index 534221ece3a..31c620c94ad 100644
--- a/test/editor/parts-test.ts
+++ b/test/editor/parts-test.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { EmojiPart, PlainPart } from "../../src/editor/parts";
+import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
+
+import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts";
+import DMRoomMap from "../../src/utils/DMRoomMap";
+import { stubClient } from "../test-utils";
import { createPartCreator } from "./mock";
describe("editor/parts", () => {
@@ -40,3 +44,67 @@ describe("editor/parts", () => {
expect(() => part.toDOMNode()).not.toThrow();
});
});
+
+describe("UserPillPart", () => {
+ const roomId = "!room:example.com";
+ let client: MatrixClient;
+ let room: Room;
+ let creator: PartCreator;
+
+ beforeEach(() => {
+ client = stubClient();
+ room = new Room(roomId, client, "@me:example.com");
+ creator = new PartCreator(room, client);
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ jest.spyOn(room, "getMember").mockReturnValue(new RoomMember(room.roomId, "@user:example.com"));
+ const pill = creator.userPill("DisplayName", "@user:example.com");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ const member = new RoomMember(room.roomId, "@user:example.com");
+ jest.spyOn(room, "getMember").mockReturnValue(member);
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatar.png");
+
+ const pill = creator.userPill("DisplayName", "@user:example.com");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+});
+
+describe("RoomPillPart", () => {
+ const roomId = "!room:example.com";
+ let client: jest.Mocked
;
+ let room: Room;
+ let creator: PartCreator;
+
+ beforeEach(() => {
+ client = stubClient() as jest.Mocked;
+ DMRoomMap.makeShared();
+
+ room = new Room(roomId, client, "@me:example.com");
+ client.getRoom.mockReturnValue(room);
+ creator = new PartCreator(room, client);
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(null);
+ const pill = creator.roomPill("super-secret clubhouse");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatars/room1.jpeg");
+ const pill = creator.roomPill("cool chat club");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+});
From c7b01af49e648482707792d4292a6e7f211feb2c Mon Sep 17 00:00:00 2001
From: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
Date: Mon, 30 Jan 2023 10:54:05 +0100
Subject: [PATCH 11/28] Should open new 1:1 chat room after leaving the old one
(#9880)
* should open new 1:1 chat room after leaving the old one
Signed-off-by: Ahmad Kadri
* change the copyright
* update the test
Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
---------
Signed-off-by: Ahmad Kadri
Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
Co-authored-by: Oliver Sand
Co-authored-by: Dominik Henneke
---
.../one-to-one-chat/one-to-one-chat.spec.ts | 61 +++++++++++++++++++
src/components/structures/MatrixChat.tsx | 8 +--
2 files changed, 65 insertions(+), 4 deletions(-)
create mode 100644 cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
diff --git a/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
new file mode 100644
index 00000000000..897b916105e
--- /dev/null
+++ b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
@@ -0,0 +1,61 @@
+/*
+Copyright 2023 Ahmad Kadri
+Copyright 2023 Nordeck IT + Consulting GmbH.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { Credentials } from "../../support/homeserver";
+
+describe("1:1 chat room", () => {
+ let homeserver: HomeserverInstance;
+ let user2: Credentials;
+
+ const username = "user1234";
+ const password = "p4s5W0rD";
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Jeff");
+ cy.registerUser(homeserver, username, password).then((credential) => {
+ user2 = credential;
+ cy.visit(`/#/user/${user2.userId}?action=chat`);
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should open new 1:1 chat room after leaving the old one", () => {
+ // leave 1:1 chat room
+ cy.contains(".mx_RoomHeader_nametext", username).click();
+ cy.contains('[role="menuitem"]', "Leave").click();
+ cy.get('[data-testid="dialog-primary-button"]').click();
+
+ // wait till the room was left
+ cy.get('[role="group"][aria-label="Historical"]').within(() => {
+ cy.contains(".mx_RoomTile", username);
+ });
+
+ // open new 1:1 chat room
+ cy.visit(`/#/user/${user2.userId}?action=chat`);
+ cy.contains(".mx_RoomHeader_nametext", username);
+ });
+});
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 429adb6f504..e517aaaf83d 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -138,6 +138,7 @@ import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"
import GenericToast from "../views/toasts/GenericToast";
import { Linkify } from "../views/elements/Linkify";
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
+import { findDMForUser } from "../../utils/dm/findDMForUser";
// legacy export
export { default as Views } from "../../Views";
@@ -1101,13 +1102,12 @@ export default class MatrixChat extends React.PureComponent {
// TODO: Immutable DMs replaces this
const client = MatrixClientPeg.get();
- const dmRoomMap = new DMRoomMap(client);
- const dmRooms = dmRoomMap.getDMRoomsForUserId(userId);
+ const dmRoom = findDMForUser(client, userId);
- if (dmRooms.length > 0) {
+ if (dmRoom) {
dis.dispatch({
action: Action.ViewRoom,
- room_id: dmRooms[0],
+ room_id: dmRoom.roomId,
metricsTrigger: "MessageUser",
});
} else {
From a21929dba00491e026926d2cdea51a4457b18313 Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Mon, 30 Jan 2023 10:02:32 +0000
Subject: [PATCH 12/28] Convert RoomCreate to a functional component (#9999)
---
src/components/views/messages/RoomCreate.tsx | 78 +++++++++----------
src/events/EventTileFactory.tsx | 4 +-
.../views/messages/RoomCreate-test.tsx | 2 +-
3 files changed, 42 insertions(+), 42 deletions(-)
diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx
index 8bff5dfdcc5..25cfb90a488 100644
--- a/src/components/views/messages/RoomCreate.tsx
+++ b/src/components/views/messages/RoomCreate.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { useCallback } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
@@ -36,44 +36,44 @@ interface IProps {
* A message tile showing that this room was created as an upgrade of a previous
* room.
*/
-export default class RoomCreate extends React.Component {
- private onLinkClicked = (e: React.MouseEvent): void => {
- e.preventDefault();
+export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => {
+ const onLinkClicked = useCallback(
+ (e: React.MouseEvent): void => {
+ e.preventDefault();
- const predecessor = this.props.mxEvent.getContent()["predecessor"];
+ const predecessor = mxEvent.getContent()["predecessor"];
- dis.dispatch({
- action: Action.ViewRoom,
- event_id: predecessor["event_id"],
- highlighted: true,
- room_id: predecessor["room_id"],
- metricsTrigger: "Predecessor",
- metricsViaKeyboard: e.type !== "click",
- });
- };
-
- public render(): JSX.Element {
- const predecessor = this.props.mxEvent.getContent()["predecessor"];
- if (predecessor === undefined) {
- return ; // We should never have been instantiated in this case
- }
- const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]);
- const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]);
- permalinkCreator.load();
- const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]);
- const link = (
-
- {_t("Click here to see older messages.")}
-
- );
-
- return (
-
- );
+ dis.dispatch({
+ action: Action.ViewRoom,
+ event_id: predecessor["event_id"],
+ highlighted: true,
+ room_id: predecessor["room_id"],
+ metricsTrigger: "Predecessor",
+ metricsViaKeyboard: e.type !== "click",
+ });
+ },
+ [mxEvent],
+ );
+ const predecessor = mxEvent.getContent()["predecessor"];
+ if (predecessor === undefined) {
+ return ; // We should never have been instantiated in this case
}
-}
+ const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]);
+ const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]);
+ permalinkCreator.load();
+ const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]);
+ const link = (
+
+ {_t("Click here to see older messages.")}
+
+ );
+
+ return (
+
+ );
+};
diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx
index 1d30416f0f9..d6b5e18ad53 100644
--- a/src/events/EventTileFactory.tsx
+++ b/src/events/EventTileFactory.tsx
@@ -33,7 +33,7 @@ import LegacyCallEvent from "../components/views/messages/LegacyCallEvent";
import { CallEvent } from "../components/views/messages/CallEvent";
import TextualEvent from "../components/views/messages/TextualEvent";
import EncryptionEvent from "../components/views/messages/EncryptionEvent";
-import RoomCreate from "../components/views/messages/RoomCreate";
+import { RoomCreate } from "../components/views/messages/RoomCreate";
import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent";
import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore";
import { ALL_RULE_TYPES } from "../mjolnir/BanList";
@@ -101,7 +101,7 @@ const EVENT_TILE_TYPES = new Map([
const STATE_EVENT_TILE_TYPES = new Map([
[EventType.RoomEncryption, (ref, props) => ],
[EventType.RoomCanonicalAlias, TextualEventFactory],
- [EventType.RoomCreate, (ref, props) => ],
+ [EventType.RoomCreate, (_ref, props) => ],
[EventType.RoomMember, TextualEventFactory],
[EventType.RoomName, TextualEventFactory],
[EventType.RoomAvatar, (ref, props) => ],
diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx
index 31763f9ae89..09f17e2ae4f 100644
--- a/test/components/views/messages/RoomCreate-test.tsx
+++ b/test/components/views/messages/RoomCreate-test.tsx
@@ -22,7 +22,7 @@ import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import dis from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
-import RoomCreate from "../../../../src/components/views/messages/RoomCreate";
+import { RoomCreate } from "../../../../src/components/views/messages/RoomCreate";
import { stubClient } from "../../../test-utils/test-utils";
import { Action } from "../../../../src/dispatcher/actions";
From 3e2bf5640e17ff1c096a746089c87f8cf93e174a Mon Sep 17 00:00:00 2001
From: Germain
Date: Mon, 30 Jan 2023 12:20:11 +0000
Subject: [PATCH 13/28] Update to supportsThreads (#9907)
---
src/MatrixClientPeg.ts | 2 +-
src/utils/EventUtils.ts | 6 +-----
test/Notifier-test.ts | 2 +-
test/Unread-test.ts | 2 +-
test/components/structures/RoomSearchView-test.tsx | 2 +-
test/components/structures/ThreadPanel-test.tsx | 2 +-
test/components/structures/ThreadView-test.tsx | 2 +-
test/components/structures/TimelinePanel-test.tsx | 4 ++--
.../components/views/right_panel/RoomHeaderButtons-test.tsx | 2 +-
test/components/views/rooms/EventTile-test.tsx | 2 +-
.../NotificationBadge/UnreadNotificationBadge-test.tsx | 2 +-
test/components/views/settings/Notifications-test.tsx | 2 +-
test/stores/RoomViewStore-test.ts | 2 +-
test/test-utils/test-utils.ts | 2 +-
test/utils/EventUtils-test.ts | 2 +-
15 files changed, 16 insertions(+), 20 deletions(-)
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index f674892bf7c..19a9eb5fde8 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -218,7 +218,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
opts.pendingEventOrdering = PendingEventOrdering.Detached;
opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
- opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadenabled");
+ opts.threadSupport = SettingsStore.getValue("feature_threadenabled");
if (SettingsStore.getValue("feature_sliding_sync")) {
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index e80ffb83e20..5e650018a0e 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -229,11 +229,7 @@ export async function fetchInitialEvent(
initialEvent = null;
}
- if (
- client.supportsExperimentalThreads() &&
- initialEvent?.isRelation(THREAD_RELATION_TYPE.name) &&
- !initialEvent.getThread()
- ) {
+ if (client.supportsThreads() && initialEvent?.isRelation(THREAD_RELATION_TYPE.name) && !initialEvent.getThread()) {
const threadId = initialEvent.threadRootId;
const room = client.getRoom(roomId);
const mapper = client.getEventMapper();
diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts
index 20b2c2e3610..e2b867ee7f5 100644
--- a/test/Notifier-test.ts
+++ b/test/Notifier-test.ts
@@ -109,7 +109,7 @@ describe("Notifier", () => {
decryptEventIfNeeded: jest.fn(),
getRoom: jest.fn(),
getPushActionsForEvent: jest.fn(),
- supportsExperimentalThreads: jest.fn().mockReturnValue(false),
+ supportsThreads: jest.fn().mockReturnValue(false),
});
mockClient.pushRules = {
diff --git a/test/Unread-test.ts b/test/Unread-test.ts
index 8ff759b142b..4cfb7f265a7 100644
--- a/test/Unread-test.ts
+++ b/test/Unread-test.ts
@@ -124,7 +124,7 @@ describe("Unread", () => {
const myId = client.getUserId()!;
beforeAll(() => {
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
});
beforeEach(() => {
diff --git a/test/components/structures/RoomSearchView-test.tsx b/test/components/structures/RoomSearchView-test.tsx
index 26786956b5e..35e297b7a1a 100644
--- a/test/components/structures/RoomSearchView-test.tsx
+++ b/test/components/structures/RoomSearchView-test.tsx
@@ -48,7 +48,7 @@ describe("", () => {
beforeEach(async () => {
stubClient();
client = MatrixClientPeg.get();
- client.supportsExperimentalThreads = jest.fn().mockReturnValue(true);
+ client.supportsThreads = jest.fn().mockReturnValue(true);
room = new Room("!room:server", client, client.getUserId());
mocked(client.getRoom).mockReturnValue(room);
permalinkCreator = new RoomPermalinkCreator(room, room.roomId);
diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx
index b868549da4e..20c6cf03973 100644
--- a/test/components/structures/ThreadPanel-test.tsx
+++ b/test/components/structures/ThreadPanel-test.tsx
@@ -161,7 +161,7 @@ describe("ThreadPanel", () => {
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
- jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
diff --git a/test/components/structures/ThreadView-test.tsx b/test/components/structures/ThreadView-test.tsx
index c578121e461..ef6b195a2dc 100644
--- a/test/components/structures/ThreadView-test.tsx
+++ b/test/components/structures/ThreadView-test.tsx
@@ -117,7 +117,7 @@ describe("ThreadView", () => {
stubClient();
mockPlatformPeg();
mockClient = mocked(MatrixClientPeg.get());
- jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx
index e66a015a5c5..eaeb19d8ec0 100644
--- a/test/components/structures/TimelinePanel-test.tsx
+++ b/test/components/structures/TimelinePanel-test.tsx
@@ -362,7 +362,7 @@ describe("TimelinePanel", () => {
client = MatrixClientPeg.get();
Thread.hasServerSideSupport = FeatureSupport.Stable;
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "feature_threadenabled") return true;
@@ -524,7 +524,7 @@ describe("TimelinePanel", () => {
const client = MatrixClientPeg.get();
client.isRoomEncrypted = () => true;
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
client.decryptEventIfNeeded = () => Promise.resolve();
const authorId = client.getUserId()!;
const room = new Room("roomId", client, authorId, {
diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx
index 06192ccc232..8df11a76028 100644
--- a/test/components/views/right_panel/RoomHeaderButtons-test.tsx
+++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx
@@ -38,7 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
stubClient();
client = MatrixClientPeg.get();
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx
index c3c0b55ab5b..e3f28d851ae 100644
--- a/test/components/views/rooms/EventTile-test.tsx
+++ b/test/components/views/rooms/EventTile-test.tsx
@@ -92,7 +92,7 @@ describe("EventTile", () => {
describe("EventTile thread summary", () => {
beforeEach(() => {
- jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(client, "supportsThreads").mockReturnValue(true);
});
it("removes the thread summary when thread is deleted", async () => {
diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
index cfa44165765..9c60d26e265 100644
--- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
+++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
@@ -48,7 +48,7 @@ describe("UnreadNotificationBadge", () => {
}
beforeAll(() => {
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
});
beforeEach(() => {
diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx
index b33f72838ad..aedb96fb130 100644
--- a/test/components/views/settings/Notifications-test.tsx
+++ b/test/components/views/settings/Notifications-test.tsx
@@ -225,7 +225,7 @@ describe("", () => {
}),
setAccountData: jest.fn(),
sendReadReceipt: jest.fn(),
- supportsExperimentalThreads: jest.fn().mockReturnValue(true),
+ supportsThreads: jest.fn().mockReturnValue(true),
});
mockClient.getPushRules.mockResolvedValue(pushRules);
diff --git a/test/stores/RoomViewStore-test.ts b/test/stores/RoomViewStore-test.ts
index 7c2b1ec81b3..370fb95bcd8 100644
--- a/test/stores/RoomViewStore-test.ts
+++ b/test/stores/RoomViewStore-test.ts
@@ -93,7 +93,7 @@ describe("RoomViewStore", function () {
getSafeUserId: jest.fn().mockReturnValue(userId),
getDeviceId: jest.fn().mockReturnValue("ABC123"),
sendStateEvent: jest.fn().mockResolvedValue({}),
- supportsExperimentalThreads: jest.fn(),
+ supportsThreads: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
const room2 = new Room(roomId2, mockClient, userId);
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index ef7bc1dcef3..5938f89cfe3 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -166,7 +166,7 @@ export function createTestClient(): MatrixClient {
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
getCapabilities: jest.fn().mockResolvedValue({}),
- supportsExperimentalThreads: () => false,
+ supportsThreads: () => false,
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts
index decf42931a8..9c4012a4c4c 100644
--- a/test/utils/EventUtils-test.ts
+++ b/test/utils/EventUtils-test.ts
@@ -436,7 +436,7 @@ describe("EventUtils", () => {
pendingEventOrdering: PendingEventOrdering.Detached,
});
- jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
return events[eventId] ?? Promise.reject();
From 4c1e4f5127bd7fc8a3ab852d630242a704ffbda2 Mon Sep 17 00:00:00 2001
From: Clark Fischer <439978+clarkf@users.noreply.github.com>
Date: Mon, 30 Jan 2023 14:31:32 +0000
Subject: [PATCH 14/28] Fix "[object Promise]" appearing in HTML exports
(#9975)
Fixes /~https://github.com/vector-im/element-web/issues/24272
---
src/DateUtils.ts | 2 +-
src/components/structures/MessagePanel.tsx | 4 +-
.../dialogs/MessageEditHistoryDialog.tsx | 2 +-
.../views/rooms/SearchResultTile.tsx | 7 +-
src/utils/exportUtils/HtmlExport.tsx | 43 ++-
src/utils/exportUtils/exportCSS.ts | 4 +-
.../dialogs/MessageEditHistoryDialog-test.tsx | 83 +++++
.../MessageEditHistoryDialog-test.tsx.snap | 322 ++++++++++++++++++
.../views/rooms/SearchResultTile-test.tsx | 106 +++---
test/test-utils/test-utils.ts | 4 +
test/utils/exportUtils/HTMLExport-test.ts | 286 +++++++++++++++-
.../__snapshots__/HTMLExport-test.ts.snap | 86 +++++
test/utils/exportUtils/exportCSS-test.ts | 26 ++
13 files changed, 893 insertions(+), 82 deletions(-)
create mode 100644 test/components/views/dialogs/MessageEditHistoryDialog-test.tsx
create mode 100644 test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap
create mode 100644 test/utils/exportUtils/exportCSS-test.ts
diff --git a/src/DateUtils.ts b/src/DateUtils.ts
index 5973a7c5f2e..c1aa69aacd6 100644
--- a/src/DateUtils.ts
+++ b/src/DateUtils.ts
@@ -175,7 +175,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean {
return prevDate.getFullYear() === nextDate.getFullYear();
}
-export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
+export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean {
if (!nextEventDate || !prevEventDate) {
return false;
}
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 98e8f79ec72..2dd432cb928 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -72,7 +72,7 @@ const groupedStateEvents = [
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
export function shouldFormContinuation(
- prevEvent: MatrixEvent,
+ prevEvent: MatrixEvent | null,
mxEvent: MatrixEvent,
showHiddenEvents: boolean,
threadsEnabled: boolean,
@@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component {
// here.
return !this.props.canBackPaginate;
}
- return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
+ return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate);
}
// Get a list of read receipts that should be shown next to this event
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
index 943e7f58d2d..8775b4eb5c3 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
- if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
+ if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) {
nodes.push(
diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx
index 067cbaee383..3ec68b989f4 100644
--- a/src/components/views/rooms/SearchResultTile.tsx
+++ b/src/components/views/rooms/SearchResultTile.tsx
@@ -84,7 +84,7 @@ export default class SearchResultTile extends React.Component {
// is this a continuation of the previous message?
const continuation =
prevEv &&
- !wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) &&
+ !wantsDateSeparator(prevEv.getDate() || undefined, mxEv.getDate() || undefined) &&
shouldFormContinuation(
prevEv,
mxEv,
@@ -96,7 +96,10 @@ export default class SearchResultTile extends React.Component {
let lastInSection = true;
const nextEv = timeline[j + 1];
if (nextEv) {
- const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate());
+ const willWantDateSeparator = wantsDateSeparator(
+ mxEv.getDate() || undefined,
+ nextEv.getDate() || undefined,
+ );
lastInSection =
willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx
index 6b4240375c6..e915d180250 100644
--- a/src/utils/exportUtils/HtmlExport.tsx
+++ b/src/utils/exportUtils/HtmlExport.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ export default class HTMLExporter extends Exporter {
}
protected async getRoomAvatar(): Promise {
- let blob: Blob;
+ let blob: Blob | undefined = undefined;
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
const avatarPath = "room.png";
if (avatarUrl) {
@@ -85,7 +85,7 @@ export default class HTMLExporter extends Exporter {
height={32}
name={this.room.name}
title={this.room.name}
- url={blob ? avatarPath : null}
+ url={blob ? avatarPath : ""}
resizeMethod="crop"
/>
);
@@ -96,9 +96,9 @@ export default class HTMLExporter extends Exporter {
const roomAvatar = await this.getRoomAvatar();
const exportDate = formatFullDateNoDayNoTime(new Date());
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
- const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator;
- const exporter = this.client.getUserId();
- const exporterName = this.room?.getMember(exporter)?.rawDisplayName;
+ const creatorName = (creator ? this.room.getMember(creator)?.rawDisplayName : creator) || creator;
+ const exporter = this.client.getUserId()!;
+ const exporterName = this.room.getMember(exporter)?.rawDisplayName;
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
const createdText = _t("%(creatorName)s created this room.", {
creatorName,
@@ -217,20 +217,19 @@ export default class HTMLExporter extends Exporter {