Compare commits
5 Commits
hs/user-pr
...
renovate/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e670bfe41 | ||
|
|
cb7382f235 | ||
|
|
f796dce34b | ||
|
|
523783706f | ||
|
|
0f530f6c01 |
@@ -27,7 +27,7 @@ Element has several tiers of support for different environments:
|
||||
- Best effort
|
||||
- Definition:
|
||||
- Issues **accepted**, regressions **do not block** the release
|
||||
- The wider Element Products (including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
||||
- The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
||||
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
|
||||
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
|
||||
- Community Supported
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.1.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.39.0",
|
||||
@@ -152,7 +153,8 @@
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
@@ -305,7 +307,7 @@
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^11.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "5.9.2",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^7.0.1",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
|
||||
@@ -11,3 +11,50 @@ index 917a7fc..a2710c6 100644
|
||||
didOkOrSubmit: boolean;
|
||||
model: M;
|
||||
}>;
|
||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
|
||||
index cb5f2e5..51daa51 100644
|
||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
|
||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
|
||||
@@ -66,23 +66,23 @@ export interface SetupEncryptionStoreProjection {
|
||||
export interface ProvideCryptoSetupExtensions {
|
||||
examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
|
||||
persistCredentials(credentials: ExtendedMatrixClientCreds): void;
|
||||
- getSecretStorageKey(): Uint8Array | null;
|
||||
- createSecretStorageKey(): Uint8Array | null;
|
||||
+ getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
+ createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
catchAccessSecretStorageError(e: Error): void;
|
||||
setupEncryptionNeeded: (args: CryptoSetupArgs) => boolean;
|
||||
/** @deprecated This callback is no longer used by matrix-react-sdk */
|
||||
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
|
||||
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
|
||||
SHOW_ENCRYPTION_SETUP_UI: boolean;
|
||||
}
|
||||
export declare abstract class CryptoSetupExtensionsBase implements ProvideCryptoSetupExtensions {
|
||||
abstract examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
|
||||
abstract persistCredentials(credentials: ExtendedMatrixClientCreds): void;
|
||||
- abstract getSecretStorageKey(): Uint8Array | null;
|
||||
- abstract createSecretStorageKey(): Uint8Array | null;
|
||||
+ abstract getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
+ abstract createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
abstract catchAccessSecretStorageError(e: Error): void;
|
||||
abstract setupEncryptionNeeded(args: CryptoSetupArgs): boolean;
|
||||
/** `getDehydrationKeyCallback` is no longer used; we provide an empty impl for type compatibility. */
|
||||
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
|
||||
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
|
||||
abstract SHOW_ENCRYPTION_SETUP_UI: boolean;
|
||||
}
|
||||
export interface CryptoSetupArgs {
|
||||
@@ -98,9 +98,9 @@ export declare class DefaultCryptoSetupExtensions extends CryptoSetupExtensionsB
|
||||
SHOW_ENCRYPTION_SETUP_UI: boolean;
|
||||
examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
|
||||
persistCredentials(credentials: ExtendedMatrixClientCreds): void;
|
||||
- getSecretStorageKey(): Uint8Array | null;
|
||||
- createSecretStorageKey(): Uint8Array | null;
|
||||
+ getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
+ createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
|
||||
catchAccessSecretStorageError(e: Error): void;
|
||||
setupEncryptionNeeded(args: CryptoSetupArgs): boolean;
|
||||
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
|
||||
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ test.describe("Room list filters and sort", () => {
|
||||
So we expect 'Old Room' to show up in the room list.
|
||||
*/
|
||||
const roomListView = getRoomList(page);
|
||||
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
|
||||
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||
await expect(oldRoomTile).toBeVisible();
|
||||
|
||||
/*
|
||||
@@ -139,9 +139,8 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" });
|
||||
// item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded
|
||||
await app.scrollListToBottom(roomListView);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
@@ -152,7 +151,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("listbox", { name: "Room list", exact: true })
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
@@ -228,37 +227,37 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5);
|
||||
|
||||
await getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await getFilterCollapseButton(page).click();
|
||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
||||
@@ -269,7 +268,6 @@ test.describe("Room list filters and sort", () => {
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
@@ -278,20 +276,20 @@ test.describe("Room list filters and sort", () => {
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
||||
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||
await expect(unreadDm).toBeVisible();
|
||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -301,7 +299,7 @@ test.describe("Room list filters and sort", () => {
|
||||
await getRoomOptionsMenu(page).click();
|
||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
||||
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
|
||||
});
|
||||
|
||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||
@@ -309,7 +307,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
||||
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ test.describe("Room list panel", () => {
|
||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
// Wait for the last room to be visible
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
});
|
||||
|
||||
|
||||
@@ -43,35 +43,31 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
||||
await page.getByRole("button", { name: "User menu" }).hover();
|
||||
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" });
|
||||
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@@ -101,7 +97,7 @@ test.describe("Room list", () => {
|
||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@@ -121,10 +117,10 @@ test.describe("Room list", () => {
|
||||
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
@@ -143,25 +139,25 @@ test.describe("Room list", () => {
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("option", { name: "Open room room0" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Shortcuts", () => {
|
||||
test("should select the next room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||
@@ -169,7 +165,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the previous room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
@@ -177,7 +173,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the last room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||
@@ -191,7 +187,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
@@ -203,8 +199,8 @@ test.describe("Room list", () => {
|
||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||
|
||||
// open the room
|
||||
await room29.click();
|
||||
@@ -223,7 +219,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||
|
||||
@@ -262,7 +258,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("option", { name: "public room" });
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
@@ -272,7 +268,7 @@ test.describe("Room list", () => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "low priority room", visibility: "public" });
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
|
||||
|
||||
// Make room low priority
|
||||
await publicRoom.hover();
|
||||
@@ -297,7 +293,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("option", { name: "video room" });
|
||||
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
@@ -316,7 +312,7 @@ test.describe("Room list", () => {
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
@@ -331,7 +327,7 @@ test.describe("Room list", () => {
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "2 notifications" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
@@ -362,7 +358,7 @@ test.describe("Room list", () => {
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mention" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
@@ -383,7 +379,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||
});
|
||||
@@ -410,7 +406,7 @@ test.describe("Room list", () => {
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
@@ -422,7 +418,7 @@ test.describe("Room list", () => {
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
@@ -445,7 +441,7 @@ test.describe("Room list", () => {
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "silent" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
@@ -10,7 +10,7 @@ import {
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "main@sha256:430b1f00e74c3f89f078670f676b4333f6bbe5a339962344b3ae84e99e9bcd7f";
|
||||
const TAG = "main@sha256:e1004d0213a985064766c99df344b0ac799869aff503be5aba1a720478258873";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
|
||||
const TAG = "develop@sha256:78b32934c4a7a616ada0a0af6e6ba3a102f97ff747fc0d2d1e3bd930e16e529e";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -142,7 +142,6 @@
|
||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.pcss";
|
||||
@import "./views/dialogs/_InviteDialog.pcss";
|
||||
@import "./views/dialogs/_InviteProgressBody.pcss";
|
||||
@import "./views/dialogs/_JoinRuleDropdown.pcss";
|
||||
@import "./views/dialogs/_LeaveSpaceDialog.pcss";
|
||||
@import "./views/dialogs/_LocationViewDialog.pcss";
|
||||
|
||||
@@ -63,6 +63,17 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 25px;
|
||||
line-height: $font-25px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_buttonAndSpinner {
|
||||
.mx_Spinner {
|
||||
/* Width and height are required to trick the layout engine. */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-inline-start: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_section {
|
||||
@@ -207,10 +218,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
margin-top: var(--cpd-space-12x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_transfer {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
text-align: center;
|
||||
font: var(--cpd-font-body-lg-regular);
|
||||
|
||||
h1 {
|
||||
color: var(--cpd-color-text-primary);
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
}
|
||||
}
|
||||
@@ -15,44 +15,40 @@
|
||||
* |-------------------------------------------------------|
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
/* Remove button default style */
|
||||
background: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: unset;
|
||||
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
padding-right: var(--cpd-space-5x);
|
||||
|
||||
.mx_RoomListItemView_text {
|
||||
.mx_RoomListItemView_content {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
padding-right: var(--cpd-space-5x);
|
||||
|
||||
.mx_RoomListItemView_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mx_RoomListItemView_text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_messagePreview {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.mx_RoomListItemView_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_messagePreview {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +57,7 @@
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content {
|
||||
/**
|
||||
* The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331
|
||||
* the icon size of the menu is 18px instead of 20px with a different internal padding
|
||||
|
||||
@@ -79,12 +79,3 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
|
||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
|
||||
* @param ev The keyboard event to check
|
||||
* @returns True if the event is a modified key event, false otherwise
|
||||
*/
|
||||
export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
|
||||
return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia
|
||||
// during the same single operation. Use `accessSecretStorage` below to scope a
|
||||
// single secret storage operation, as it will clear the cached keys once the
|
||||
// operation ends.
|
||||
let secretStorageKeys: Record<string, Uint8Array> = {};
|
||||
let secretStorageKeys: Record<string, Uint8Array<ArrayBuffer>> = {};
|
||||
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
|
||||
let secretStorageBeingAccessed = false;
|
||||
|
||||
@@ -51,8 +51,8 @@ export class AccessCancelledError extends Error {
|
||||
|
||||
function makeInputToKey(
|
||||
keyInfo: SecretStorage.SecretStorageKeyDescription,
|
||||
): (keyParams: KeyParams) => Promise<Uint8Array> {
|
||||
return async ({ passphrase, recoveryKey }): Promise<Uint8Array> => {
|
||||
): (keyParams: KeyParams) => Promise<Uint8Array<ArrayBuffer>> {
|
||||
return async ({ passphrase, recoveryKey }): Promise<Uint8Array<ArrayBuffer>> => {
|
||||
if (passphrase) {
|
||||
return deriveRecoveryKeyFromPassphrase(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
|
||||
} else if (recoveryKey) {
|
||||
@@ -69,7 +69,7 @@ async function getSecretStorageKey(
|
||||
keys: Record<string, SecretStorage.SecretStorageKeyDescription>;
|
||||
},
|
||||
secretName: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
): Promise<[string, Uint8Array<ArrayBuffer>]> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
|
||||
|
||||
@@ -139,7 +139,7 @@ async function getSecretStorageKey(
|
||||
function cacheSecretStorageKey(
|
||||
keyId: string,
|
||||
keyInfo: SecretStorage.SecretStorageKeyDescription,
|
||||
key: Uint8Array,
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
): void {
|
||||
if (secretStorageBeingAccessed) {
|
||||
logger.debug(`Caching 4S key ${keyId}`);
|
||||
|
||||
@@ -124,7 +124,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||
if (keyFromCustomisations) this.initExtension(keyFromCustomisations);
|
||||
}
|
||||
|
||||
private initExtension(keyFromCustomisations: Uint8Array): void {
|
||||
private initExtension(keyFromCustomisations: Uint8Array<ArrayBuffer>): void {
|
||||
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
||||
this.recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
|
||||
@@ -142,7 +142,7 @@ export class VoiceMessageRecording implements IDestroyable {
|
||||
this.buffer = concat(this.buffer, buf);
|
||||
};
|
||||
|
||||
private get audioBuffer(): Uint8Array {
|
||||
private get audioBuffer(): Uint8Array<ArrayBuffer> {
|
||||
// We need a clone of the buffer to avoid accidentally changing the position
|
||||
// on the real thing.
|
||||
return this.buffer.slice(0);
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { useRef, type JSX, useCallback, useEffect, useState } from "react";
|
||||
import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso";
|
||||
|
||||
import { isModifiedKeyEvent, Key } from "../../Keyboard";
|
||||
/**
|
||||
* Context object passed to each list item containing the currently focused key
|
||||
* and any additional context data from the parent component.
|
||||
@@ -35,7 +34,6 @@ export interface IListViewProps<Item, Context>
|
||||
* @param index - The index of the item in the list
|
||||
* @param item - The data item to render
|
||||
* @param context - The context object containing the focused key and any additional data
|
||||
* @param onFocus - A callback that is required to be called when the item component receives focus
|
||||
* @returns JSX element representing the rendered item
|
||||
*/
|
||||
getItemComponent: (
|
||||
@@ -64,14 +62,6 @@ export interface IListViewProps<Item, Context>
|
||||
* @return The key to use for focusing the item
|
||||
*/
|
||||
getItemKey: (item: Item) => string;
|
||||
/**
|
||||
* Callback function to handle key down events on the list container.
|
||||
* ListView handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown)
|
||||
* and stops propagation otherwise the event bubbles and this callback is called for the use of the parent.
|
||||
* @param e - The keyboard event
|
||||
* @returns
|
||||
*/
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +73,7 @@ export interface IListViewProps<Item, Context>
|
||||
*/
|
||||
export function ListView<Item, Context = any>(props: IListViewProps<Item, Context>): React.ReactElement {
|
||||
// Extract our custom props to avoid conflicts with Virtuoso props
|
||||
const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props;
|
||||
const { items, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
|
||||
/** Reference to the Virtuoso component for programmatic scrolling */
|
||||
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
|
||||
/** Reference to the DOM element containing the virtualized list */
|
||||
@@ -135,7 +125,7 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
const key = getItemKey(items[clampedIndex]);
|
||||
setTabIndexKey(key);
|
||||
isScrollingToItem.current = true;
|
||||
virtuosoHandleRef.current?.scrollIntoView({
|
||||
virtuosoHandleRef?.current?.scrollIntoView({
|
||||
index: clampedIndex,
|
||||
align: align,
|
||||
behavior: "auto",
|
||||
@@ -178,44 +168,40 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
* Supports Arrow keys, Home, End, Page Up/Down, Enter, and Space.
|
||||
*/
|
||||
const keyDownCallback = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
|
||||
let handled = false;
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (!e) return; // Guard against null/undefined events
|
||||
|
||||
// Guard against null/undefined events and modified keys which we don't want to handle here but do
|
||||
// at the settings level shortcuts(E.g. Select next room, etc )
|
||||
if (e || !isModifiedKeyEvent(e)) {
|
||||
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if (e.code === Key.HOME) {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === Key.END) {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
}
|
||||
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
|
||||
|
||||
let handled = false;
|
||||
if (e.code === "ArrowUp" && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === "ArrowDown" && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if (e.code === "Home") {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === "End") {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === "PageDown" && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === "PageUp" && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
},
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onKeyDown],
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -265,12 +251,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
[keyToIndexMap, visibleRange, scrollToIndex, tabIndexKey],
|
||||
);
|
||||
|
||||
const onBlur = useCallback((event: React.FocusEvent<HTMLDivElement>): void => {
|
||||
// Only set isFocused to false if the focus is moving outside the list
|
||||
// This prevents the list from losing focus when interacting with menus inside it
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
const onBlur = useCallback((): void => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
const listContext: ListContext<Context> = {
|
||||
@@ -282,8 +264,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
return (
|
||||
<Virtuoso
|
||||
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default
|
||||
ref={virtuosoHandleRef}
|
||||
scrollerRef={scrollerRef}
|
||||
ref={virtuosoHandleRef}
|
||||
onKeyDown={keyDownCallback}
|
||||
context={listContext}
|
||||
rangeChanged={setVisibleRange}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useStickyRoomList } from "./useStickyRoomList";
|
||||
import { useRoomListNavigation } from "./useRoomListNavigation";
|
||||
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
|
||||
export interface RoomListViewState {
|
||||
/**
|
||||
@@ -27,9 +26,9 @@ export interface RoomListViewState {
|
||||
isLoadingRooms: boolean;
|
||||
|
||||
/**
|
||||
* The room results to be displayed (along with the spaceId and filter keys at the time of query)
|
||||
* A list of rooms to be displayed in the left panel.
|
||||
*/
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
|
||||
/**
|
||||
* Create a chat room
|
||||
@@ -72,10 +71,10 @@ export interface RoomListViewState {
|
||||
*/
|
||||
export function useRoomListViewModel(): RoomListViewState {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const { isLoadingRooms, primaryFilters, activePrimaryFilter, roomsResult: filteredRooms } = useFilteredRooms();
|
||||
const { activeIndex, roomsResult } = useStickyRoomList(filteredRooms);
|
||||
const { isLoadingRooms, primaryFilters, activePrimaryFilter, rooms: filteredRooms } = useFilteredRooms();
|
||||
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
|
||||
|
||||
useRoomListNavigation(roomsResult.rooms);
|
||||
useRoomListNavigation(rooms);
|
||||
|
||||
const currentSpace = useEventEmitterState<Room | null>(
|
||||
SpaceStore.instance,
|
||||
@@ -89,7 +88,7 @@ export function useRoomListViewModel(): RoomListViewState {
|
||||
|
||||
return {
|
||||
isLoadingRooms,
|
||||
roomsResult,
|
||||
rooms,
|
||||
canCreateRoom,
|
||||
createRoom,
|
||||
createChatRoom,
|
||||
|
||||
@@ -5,15 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters";
|
||||
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import RoomListStoreV3, {
|
||||
LISTS_LOADED_EVENT,
|
||||
LISTS_UPDATE_EVENT,
|
||||
type RoomsResult,
|
||||
} from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
|
||||
@@ -38,7 +35,7 @@ export interface PrimaryFilter {
|
||||
interface FilteredRooms {
|
||||
primaryFilters: PrimaryFilter[];
|
||||
isLoadingRooms: boolean;
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
/**
|
||||
* The currently active primary filter.
|
||||
* If no primary filter is active, this will be undefined.
|
||||
@@ -66,12 +63,12 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
*/
|
||||
const [primaryFilter, setPrimaryFilter] = useState<FilterKey | undefined>();
|
||||
|
||||
const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
|
||||
const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
|
||||
const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms);
|
||||
|
||||
const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => {
|
||||
const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters);
|
||||
setRoomsResult(newRooms);
|
||||
setRooms(newRooms);
|
||||
}, []);
|
||||
|
||||
// Reset filters when active space changes
|
||||
@@ -80,15 +77,9 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
|
||||
array.filter((f) => f !== undefined) as FilterKey[];
|
||||
|
||||
const getAppliedFilters = useCallback((): FilterKey[] => {
|
||||
const getAppliedFilters = (): FilterKey[] => {
|
||||
return filterUndefined([primaryFilter]);
|
||||
}, [primaryFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the rooms state when the primary filter changes
|
||||
const filters = getAppliedFilters();
|
||||
updateRoomsFromStore(filters);
|
||||
}, [getAppliedFilters, updateRoomsFromStore]);
|
||||
};
|
||||
|
||||
useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => {
|
||||
const filters = getAppliedFilters();
|
||||
@@ -131,6 +122,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
isLoadingRooms,
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
roomsResult,
|
||||
rooms,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Optional } from "matrix-events-sdk";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
|
||||
function getIndexByRoomId(rooms: Room[], roomId: Optional<string>): number | undefined {
|
||||
const index = rooms.findIndex((room) => room.roomId === roomId);
|
||||
@@ -68,11 +67,11 @@ function getRoomsWithStickyRoom(
|
||||
return { newIndex: oldIndex, newRooms };
|
||||
}
|
||||
|
||||
export interface StickyRoomListResult {
|
||||
interface StickyRoomListResult {
|
||||
/**
|
||||
* The rooms result with the active sticky room applied
|
||||
* List of rooms with sticky active room.
|
||||
*/
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
/**
|
||||
* Index of the active room in the room list.
|
||||
*/
|
||||
@@ -86,10 +85,10 @@ export interface StickyRoomListResult {
|
||||
* @param rooms list of rooms
|
||||
* @see {@link StickyRoomListResult} details what this hook returns..
|
||||
*/
|
||||
export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult {
|
||||
const [listState, setListState] = useState<StickyRoomListResult>({
|
||||
activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()),
|
||||
roomsResult: roomsResult,
|
||||
export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
|
||||
const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({
|
||||
index: undefined,
|
||||
roomsWithStickyRoom: rooms,
|
||||
});
|
||||
|
||||
const currentSpaceRef = useRef(SpaceStore.instance.activeSpace);
|
||||
@@ -98,18 +97,13 @@ export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResul
|
||||
(newRoomId: string | null, isRoomChange: boolean = false) => {
|
||||
setListState((current) => {
|
||||
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId);
|
||||
const oldIndex = current.activeIndex;
|
||||
const { newIndex, newRooms } = getRoomsWithStickyRoom(
|
||||
roomsResult.rooms,
|
||||
oldIndex,
|
||||
newActiveIndex,
|
||||
isRoomChange,
|
||||
);
|
||||
return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } };
|
||||
const newActiveIndex = getIndexByRoomId(rooms, activeRoomId);
|
||||
const oldIndex = current.index;
|
||||
const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange);
|
||||
return { index: newIndex, roomsWithStickyRoom: newRooms };
|
||||
});
|
||||
},
|
||||
[roomsResult],
|
||||
[rooms],
|
||||
);
|
||||
|
||||
// Re-calculate the index when the active room has changed.
|
||||
@@ -121,19 +115,20 @@ export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResul
|
||||
useEffect(() => {
|
||||
let newRoomId: string | null = null;
|
||||
let isRoomChange = false;
|
||||
if (currentSpaceRef.current !== roomsResult.spaceId) {
|
||||
const newSpace = SpaceStore.instance.activeSpace;
|
||||
if (currentSpaceRef.current !== newSpace) {
|
||||
/*
|
||||
If the space has changed, we check if we can immediately set the active
|
||||
index to the last opened room in that space. Otherwise, we might see a
|
||||
flicker because of the delay between the space change event and
|
||||
active room change dispatch.
|
||||
*/
|
||||
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId);
|
||||
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpace);
|
||||
isRoomChange = true;
|
||||
currentSpaceRef.current = roomsResult.spaceId;
|
||||
currentSpaceRef.current = newSpace;
|
||||
}
|
||||
updateRoomsAndIndex(newRoomId, isRoomChange);
|
||||
}, [roomsResult, updateRoomsAndIndex]);
|
||||
}, [rooms, updateRoomsAndIndex]);
|
||||
|
||||
return listState;
|
||||
return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom };
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import Field from "../elements/Field";
|
||||
import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView";
|
||||
import Dialpad from "../voip/DialPad";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
@@ -64,7 +65,6 @@ import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter";
|
||||
import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDialog";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
@@ -329,14 +329,8 @@ interface IInviteDialogState {
|
||||
dialPadValue: string;
|
||||
currentTabId: TabId;
|
||||
|
||||
/**
|
||||
* True if we are sending the invites.
|
||||
*
|
||||
* We will grey out the action button, hide the suggestions, and display a spinner.
|
||||
*/
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean;
|
||||
|
||||
/** Error from the last attempt to send invites. */
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
@@ -623,10 +617,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await inviteMultipleToRoom(cli, this.props.roomId, targetIds, {
|
||||
// We show our own progress body, so don't pop up a separate dialog.
|
||||
inhibitProgressDialog: true,
|
||||
});
|
||||
const result = await inviteMultipleToRoom(cli, this.props.roomId, targetIds);
|
||||
if (!this.shouldAbortAfterInviteError(result, room)) {
|
||||
// handles setting error message too
|
||||
this.props.onFinished(true);
|
||||
@@ -1337,6 +1328,11 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
* "CallTransfer" one.
|
||||
*/
|
||||
private renderMainTab(): JSX.Element {
|
||||
let spinner: JSX.Element | undefined;
|
||||
if (this.state.busy) {
|
||||
spinner = <Spinner w={20} h={20} />;
|
||||
}
|
||||
|
||||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn: (() => Promise<void>) | null = null;
|
||||
@@ -1441,9 +1437,12 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
<p className="mx_InviteDialog_helpText">{helpText}</p>
|
||||
<div className="mx_InviteDialog_addressBar">
|
||||
{this.renderEditor()}
|
||||
{goButton}
|
||||
<div className="mx_InviteDialog_buttonAndSpinner">
|
||||
{goButton}
|
||||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
{this.state.busy ? <InviteProgressBody /> : this.renderSuggestions()}
|
||||
{this.renderSuggestions()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
/** The common body of components that show the progress of sending room invites. */
|
||||
const InviteProgressBody: React.FC = () => {
|
||||
return (
|
||||
<div className="mx_InviteProgressBody">
|
||||
<InlineSpinner w={32} h={32} />
|
||||
<h1>{_t("invite|progress|preparing")}</h1>
|
||||
{_t("invite|progress|dont_close")}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteProgressBody;
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import Modal from "../../../Modal.tsx";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
|
||||
/** A Modal dialog that pops up while room invites are being sent. */
|
||||
const InviteProgressDialog: React.FC = (_) => {
|
||||
return <InviteProgressBody />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the invite progress dialog.
|
||||
*
|
||||
* Returns a callback which will close the dialog again.
|
||||
*/
|
||||
export function openInviteProgressDialog(): () => void {
|
||||
const onBeforeClose = async (reason?: string): Promise<boolean> => {
|
||||
// Inhibit closing via background click
|
||||
return reason != "backgroundClick";
|
||||
};
|
||||
|
||||
const { close } = Modal.createDialog(
|
||||
InviteProgressDialog,
|
||||
/* props */ {},
|
||||
/* className */ undefined,
|
||||
/* isPriorityModal */ false,
|
||||
/* isStaticModal */ false,
|
||||
{ onBeforeClose },
|
||||
);
|
||||
return close;
|
||||
}
|
||||
@@ -74,7 +74,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.state.playback?.destroy();
|
||||
this.state.audioPlayerVm?.dispose();
|
||||
}
|
||||
|
||||
protected get showFileBody(): boolean {
|
||||
|
||||
@@ -12,9 +12,7 @@ import { type MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||
import ModuleApi from "../../../modules/Api";
|
||||
import { CustomComponentsApi } from "../../../modules/customComponentApi";
|
||||
import { MessageProfileComponentProps } from "@element-hq/element-web-module-api";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick?(): void;
|
||||
@@ -26,25 +24,17 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
||||
userId: mxEvent.getSender(),
|
||||
member: mxEvent.sender,
|
||||
});
|
||||
|
||||
if (mxEvent.getContent().msgtype === MsgType.Emote) {
|
||||
return <></>;
|
||||
}
|
||||
const moduleRenderer = ModuleApi.customComponents.messageProfileRenderer;
|
||||
const renderFn = (moduleProps: MessageProfileComponentProps) => <DisambiguatedProfile
|
||||
fallbackName={moduleProps.mxEvent.sender ?? ""}
|
||||
onClick={moduleProps.onClick}
|
||||
member={moduleProps.member}
|
||||
colored={true}
|
||||
emphasizeDisplayName={true}
|
||||
withTooltip={withTooltip}
|
||||
/>;
|
||||
|
||||
const modProps = {
|
||||
onClick,
|
||||
mxEvent: CustomComponentsApi.getModuleMatrixEvent(mxEvent)!,
|
||||
member: member || undefined,
|
||||
};
|
||||
|
||||
return moduleRenderer ? moduleRenderer(modProps, renderFn) : renderFn(modProps);
|
||||
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
||||
<DisambiguatedProfile
|
||||
fallbackName={mxEvent.getSender() ?? ""}
|
||||
onClick={onClick}
|
||||
member={member}
|
||||
colored={true}
|
||||
emphasizeDisplayName={true}
|
||||
withTooltip={withTooltip}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import PresenceLabel from "../../rooms/PresenceLabel";
|
||||
import CopyableText from "../../elements/CopyableText";
|
||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||
import ModuleApi from "../../../../modules/Api";
|
||||
|
||||
export interface UserInfoHeaderViewProps {
|
||||
member: Member;
|
||||
@@ -49,27 +48,6 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const moduleRenderer = ModuleApi.customComponents.userInfoRenderer;
|
||||
let usernameSection;
|
||||
if (moduleRenderer && vm.userIdentifier) {
|
||||
usernameSection = moduleRenderer(
|
||||
{
|
||||
userId: vm.userIdentifier,
|
||||
},
|
||||
(props) => <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => props.userId} border={false}>
|
||||
{props.userId}
|
||||
</CopyableText>
|
||||
</Text>,
|
||||
);
|
||||
} else {
|
||||
usernameSection = <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||
{vm.userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="mx_UserInfo_avatar">
|
||||
@@ -105,7 +83,11 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
{usernameSection}
|
||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||
{vm.userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>
|
||||
</Flex>
|
||||
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
||||
</Container>
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type ScrollIntoViewLocation } from "react-virtuoso";
|
||||
import { isEqual } from "lodash";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
import { AutoSizer, List, type ListRowProps } from "react-virtualized";
|
||||
|
||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListItemView } from "./RoomListItemView";
|
||||
import { type ListContext, ListView } from "../../../utils/ListView";
|
||||
import { type FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
|
||||
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
|
||||
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
|
||||
@@ -29,93 +26,55 @@ interface RoomListProps {
|
||||
/**
|
||||
* A virtualized list of rooms.
|
||||
*/
|
||||
export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): JSX.Element {
|
||||
const lastSpaceId = useRef<string | undefined>(undefined);
|
||||
const lastFilterKeys = useRef<FilterKey[] | undefined>(undefined);
|
||||
const roomCount = roomsResult.rooms.length;
|
||||
const getItemComponent = useCallback(
|
||||
(
|
||||
index: number,
|
||||
item: Room,
|
||||
context: ListContext<{
|
||||
spaceId: string;
|
||||
filterKeys: FilterKey[] | undefined;
|
||||
}>,
|
||||
onFocus: (e: React.FocusEvent) => void,
|
||||
): JSX.Element => {
|
||||
const itemKey = item.roomId;
|
||||
const isRovingItem = itemKey === context.tabIndexKey;
|
||||
const isFocused = isRovingItem && context.focused;
|
||||
const isSelected = activeIndex === index;
|
||||
return (
|
||||
<RoomListItemView
|
||||
room={item}
|
||||
key={itemKey}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused}
|
||||
tabIndex={isRovingItem ? 0 : -1}
|
||||
roomIndex={index}
|
||||
roomCount={roomCount}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[activeIndex, roomCount],
|
||||
export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element {
|
||||
const roomRendererMemoized = useCallback(
|
||||
({ key, index, style }: ListRowProps) => (
|
||||
<RoomListItemView room={rooms[index]} key={key} style={style} isSelected={activeIndex === index} />
|
||||
),
|
||||
[rooms, activeIndex],
|
||||
);
|
||||
|
||||
const getItemKey = useCallback((item: Room): string => {
|
||||
return item.roomId;
|
||||
}, []);
|
||||
|
||||
const scrollIntoViewOnChange = useCallback(
|
||||
(params: {
|
||||
context: ListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>;
|
||||
}): ScrollIntoViewLocation | null | undefined | false | void => {
|
||||
const { spaceId, filterKeys } = params.context.context;
|
||||
const shouldScrollIndexIntoView =
|
||||
lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys);
|
||||
lastFilterKeys.current = filterKeys;
|
||||
lastSpaceId.current = spaceId;
|
||||
|
||||
if (shouldScrollIndexIntoView) {
|
||||
return {
|
||||
align: `start`,
|
||||
index: activeIndex || 0,
|
||||
behavior: "auto",
|
||||
};
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[activeIndex],
|
||||
);
|
||||
|
||||
const keyDownCallback = useCallback((ev: React.KeyboardEvent) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ROOM_LIST,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
|
||||
return (
|
||||
<ListView
|
||||
context={{ spaceId: roomsResult.spaceId, filterKeys: roomsResult.filterKeys }}
|
||||
scrollIntoViewOnChange={scrollIntoViewOnChange}
|
||||
initialTopMostItemIndex={activeIndex}
|
||||
data-testid="room-list"
|
||||
role="listbox"
|
||||
aria-label={_t("room_list|list_title")}
|
||||
fixedItemHeight={48}
|
||||
items={roomsResult.rooms}
|
||||
getItemComponent={getItemComponent}
|
||||
getItemKey={getItemKey}
|
||||
isItemFocusable={() => true}
|
||||
onKeyDown={keyDownCallback}
|
||||
/>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} handleUpDown={true}>
|
||||
{({ onKeyDownHandler }) => (
|
||||
<div
|
||||
className="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
onKeyDown={(ev) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (
|
||||
navAction === KeyBindingAction.NextLandmark ||
|
||||
navAction === KeyBindingAction.PreviousLandmark
|
||||
) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ROOM_LIST,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
onKeyDownHandler(ev);
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
aria-label={_t("room_list|list_title")}
|
||||
className="mx_RoomList_List"
|
||||
rowRenderer={roomRendererMemoized}
|
||||
rowCount={rooms.length}
|
||||
rowHeight={48}
|
||||
height={height}
|
||||
width={width}
|
||||
scrollToIndex={activeIndex ?? 0}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,10 +97,7 @@ interface MoreOptionContentProps {
|
||||
|
||||
export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<>
|
||||
{vm.canMarkAsRead && (
|
||||
<MenuItem
|
||||
Icon={MarkAsReadIcon}
|
||||
@@ -160,7 +157,7 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,59 +196,54 @@ function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Eleme
|
||||
const checkComponent = <CheckIcon width="24px" height="24px" color="var(--cpd-color-icon-primary)" />;
|
||||
|
||||
return (
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
>
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|default_settings")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessages)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|default_settings")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessages)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessage && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessageLoud}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|all_messages")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessageLoud && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMentionOnly}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mentions_keywords")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.MentionsOnly)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMentionOnly && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMute}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mute_room")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.Mute)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMute && checkComponent}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
{vm.isNotificationAllMessage && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessageLoud}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|all_messages")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessageLoud && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMentionOnly}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mentions_keywords")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.MentionsOnly)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMentionOnly && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMute}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mute_room")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.Mute)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMute && checkComponent}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { type JSX, memo, useCallback, useRef, useState } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
import { NotificationDecoration } from "../NotificationDecoration";
|
||||
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||
import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView";
|
||||
|
||||
interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
@@ -25,22 +26,6 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
* Whether the room is selected
|
||||
*/
|
||||
isSelected: boolean;
|
||||
/**
|
||||
* Whether the room is focused
|
||||
*/
|
||||
isFocused: boolean;
|
||||
/**
|
||||
* A callback that indicates the item has received focus
|
||||
*/
|
||||
onFocus: (e: React.FocusEvent) => void;
|
||||
/**
|
||||
* The index of the room in the list
|
||||
*/
|
||||
roomIndex: number;
|
||||
/**
|
||||
* The total number of rooms in the list
|
||||
*/
|
||||
roomCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,19 +34,18 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
export const RoomListItemView = memo(function RoomListItemView({
|
||||
room,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onFocus,
|
||||
roomIndex: index,
|
||||
roomCount: count,
|
||||
...props
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(buttonRef);
|
||||
|
||||
const vm = useRoomListItemViewModel(room);
|
||||
const [isHover, setHover] = useState(false);
|
||||
|
||||
const [isHover, setIsHoverWithDelay] = useIsHover();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
// The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown
|
||||
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
||||
const showHoverDecoration = isMenuOpen || isFocused || isHover;
|
||||
const showHoverDecoration = isMenuOpen || isHover;
|
||||
const showHoverMenu = showHoverDecoration && vm.showHoverMenu;
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
@@ -70,15 +54,8 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
setTimeout(() => setIsMenuOpen(false), 10);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
ref.current?.focus({ preventScroll: true, focusVisible: true });
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
const content = (
|
||||
<Flex
|
||||
as="button"
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames("mx_RoomListItemView", {
|
||||
mx_RoomListItemView_hover: showHoverDecoration,
|
||||
@@ -86,59 +63,63 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
mx_RoomListItemView_selected: isSelected,
|
||||
mx_RoomListItemView_bold: vm.isBold,
|
||||
})}
|
||||
gap="var(--cpd-space-3x)"
|
||||
align="center"
|
||||
type="button"
|
||||
role="option"
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={count}
|
||||
aria-selected={isSelected}
|
||||
aria-label={vm.a11yLabel}
|
||||
onClick={() => vm.openRoom()}
|
||||
onFocus={onFocus}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onBlur={() => setHover(false)}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
onMouseOver={() => setIsHoverWithDelay(true)}
|
||||
onMouseOut={() => setIsHoverWithDelay(false)}
|
||||
onFocus={() => {
|
||||
setIsHoverWithDelay(true);
|
||||
onFocus();
|
||||
}}
|
||||
// Adding a timeout because when tabbing to go to the more options and notification menu, the focus moves out of the button
|
||||
// The blur makes the button lose the hover state and these menu are not shown
|
||||
// We delay the blur event to give time to the focus to move to the menu
|
||||
onBlur={() => setIsHoverWithDelay(false, 10)}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
{...props}
|
||||
>
|
||||
<RoomAvatarView room={room} />
|
||||
<Flex
|
||||
className="mx_RoomListItemView_content"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<div className="mx_RoomListItemView_text">
|
||||
<div className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</div>
|
||||
{vm.messagePreview && (
|
||||
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
|
||||
{vm.messagePreview}
|
||||
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
|
||||
<Flex className="mx_RoomListItemView_container" gap="var(--cpd-space-3x)" align="center">
|
||||
<RoomAvatarView room={room} />
|
||||
<Flex
|
||||
className="mx_RoomListItemView_content"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<div className="mx_RoomListItemView_text">
|
||||
<div className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
hasVideoCall={vm.hasParticipantInCall}
|
||||
/>
|
||||
{vm.messagePreview && (
|
||||
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
|
||||
{vm.messagePreview}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
hasVideoCall={vm.hasParticipantInCall}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!vm.showContextMenu) return content;
|
||||
@@ -159,3 +140,33 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</RoomListItemContextMenuView>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom hook to manage the hover state of the room list item
|
||||
* If the timeout is set, it will set the hover state after the timeout
|
||||
* If the timeout is not set, it will set the hover state immediately
|
||||
* When the set method is called, it will clear any existing timeout
|
||||
*
|
||||
* @returns {boolean} isHover - The hover state
|
||||
*/
|
||||
function useIsHover(): [boolean, (value: boolean, timeout?: number) => void] {
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
// Store the timeout ID
|
||||
const timeoutRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const setIsHoverWithDelay = useCallback((value: boolean, timeout?: number): void => {
|
||||
// Clear the timeout if it exists
|
||||
clearTimeout(timeoutRef.current);
|
||||
|
||||
// No delay, set the value immediately
|
||||
if (timeout === undefined) {
|
||||
setIsHover(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a timeout to set the value after the delay
|
||||
timeoutRef.current = setTimeout(() => setIsHover(value), timeout);
|
||||
}, []);
|
||||
|
||||
return [isHover, setIsHoverWithDelay];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
*/
|
||||
export function RoomListView(): JSX.Element {
|
||||
const vm = useRoomListViewModel();
|
||||
const isRoomListEmpty = vm.roomsResult.rooms.length === 0;
|
||||
const isRoomListEmpty = vm.rooms.length === 0;
|
||||
let listBody;
|
||||
if (vm.isLoadingRooms) {
|
||||
listBody = <div className="mx_RoomListSkeleton" />;
|
||||
|
||||
@@ -18,7 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||
import RoomUpgradeWarningDialog, { type IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog";
|
||||
import { type RoomUpgradeProgress, upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { arrayHasDiff } from "../../../utils/arrays";
|
||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
@@ -120,7 +120,7 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
|
||||
opts: IFinishedOpts,
|
||||
fn: (progressText: string, progress: number, total: number) => void,
|
||||
): Promise<void> => {
|
||||
const progressCallback = (progress: RoomUpgradeProgress): void => {
|
||||
const roomId = await upgradeRoom(room, targetVersion, opts.invite, true, true, true, (progress) => {
|
||||
const total = 2 + progress.updateSpacesTotal + progress.inviteUsersTotal;
|
||||
if (!progress.roomUpgraded) {
|
||||
fn(_t("room_settings|security|join_rule_upgrade_upgrading_room"), 0, total);
|
||||
@@ -151,20 +151,7 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
|
||||
total,
|
||||
);
|
||||
}
|
||||
};
|
||||
const roomId = await upgradeRoom(
|
||||
room,
|
||||
targetVersion,
|
||||
opts.invite,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
progressCallback,
|
||||
|
||||
// We want to keep the RoomUpgradeDialog open during the upgrade, so don't replace it with the
|
||||
// invite progress dialog.
|
||||
/* inhibitInviteProgressDialog: */ true,
|
||||
);
|
||||
});
|
||||
|
||||
closeSettingsFn?.();
|
||||
|
||||
|
||||
@@ -1366,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
|
||||
"name_mxid_share_room": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
"name_mxid_share_space": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Do not close the app until finished.",
|
||||
"preparing": "Preparing invitations..."
|
||||
},
|
||||
"recents_section": "Recent Conversations",
|
||||
"room_failed_partial": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||
"room_failed_partial_title": "Some invites couldn't be sent",
|
||||
|
||||
@@ -1366,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Inviter noen ved å bruke navn, e-postadresse, brukernavn (lik <userId/>) eller <a> del dette området</a>.",
|
||||
"name_mxid_share_room": "Inviter noen ved å bruke navnet, brukernavnet (som <userId/>) eller <a>del dette rommet</a>.",
|
||||
"name_mxid_share_space": "Inviter noen ved å bruke navnet sitt, brukernavnet (lik <userId/>) eller <a> dele dette området</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Ikke lukk appen før den er ferdig.",
|
||||
"preparing": "Forbereder invitasjoner..."
|
||||
},
|
||||
"recents_section": "Nylige samtaler",
|
||||
"room_failed_partial": "Vi sendte de andre, men folkene nedenfor kunne ikke inviteres til <RoomName/>",
|
||||
"room_failed_partial_title": "Noen invitasjoner kunne ikke sendes",
|
||||
|
||||
@@ -1380,10 +1380,6 @@
|
||||
"name_email_mxid_share_space": "Pozvite niekoho pomocou jeho mena, e-mailovej adresy, používateľského mena (napríklad <userId/>) alebo <a>zdieľajte tento priestor</a>.",
|
||||
"name_mxid_share_room": "Pozvite niekoho pomocou jeho mena, používateľského mena (napríklad <userId/>) alebo <a>zdieľate túto miestnosť</a>.",
|
||||
"name_mxid_share_space": "Pozvite niekoho pomocou jeho mena, používateľského mena (napríklad <userId/>) alebo <a>zdieľajte tento priestor</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Nezatvárajte aplikáciu, kým nie je proces dokončený.",
|
||||
"preparing": "Príprava pozvánok..."
|
||||
},
|
||||
"recents_section": "Nedávne konverzácie",
|
||||
"room_failed_partial": "Ostatným sme pozvánky poslali, ale nižšie uvedené osoby nemohli byť pozvané do <RoomName/>",
|
||||
"room_failed_partial_title": "Niektoré pozvánky nebolo možné odoslať",
|
||||
@@ -1786,7 +1782,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Správca",
|
||||
"creator": "Vlastník",
|
||||
"custom": "Vlastný (%(level)s)",
|
||||
"custom_level": "Vlastná úroveň",
|
||||
"default": "Predvolené",
|
||||
@@ -1945,7 +1940,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Možnosti vlákna"
|
||||
},
|
||||
"title": "Pravý panel",
|
||||
"video_room_chat": {
|
||||
"title": "Konverzácia"
|
||||
}
|
||||
|
||||
@@ -1366,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Bjud in någon med deras namn, e-postadress eller användarnamn (som <userId/>), eller <a>dela det här rummet</a>.",
|
||||
"name_mxid_share_room": "Bjud in någon med deras namn eller användarnamn (som <userId/>) eller <a>dela det här rummet</a>.",
|
||||
"name_mxid_share_space": "Bjud in någon med deras namn eller användarnamn (som <userId/>), eller <a>dela det här utrymmet</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Stäng inte appen förrän det är klart.",
|
||||
"preparing": "Förbereder inbjudningar …"
|
||||
},
|
||||
"recents_section": "Senaste konversationerna",
|
||||
"room_failed_partial": "Vi skickade de andra, men personerna nedan kunde inte bjudas in till <RoomName/>",
|
||||
"room_failed_partial_title": "Vissa inbjudningar kunde inte skickas",
|
||||
@@ -1767,7 +1763,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Administratör",
|
||||
"creator": "Ägare",
|
||||
"custom": "Anpassad (%(level)s)",
|
||||
"custom_level": "Anpassad nivå",
|
||||
"default": "Standard",
|
||||
@@ -1920,7 +1915,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Trådalternativ"
|
||||
},
|
||||
"title": "Högerpanel",
|
||||
"video_room_chat": {
|
||||
"title": "Chatt"
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import type {
|
||||
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
|
||||
MatrixEvent as ModuleMatrixEvent,
|
||||
CustomRoomPreviewBarRenderFunction,
|
||||
MessageProfileRenderFunction,
|
||||
UserInfoRenderFunction,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
import type React from "react";
|
||||
|
||||
@@ -44,7 +42,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
* @param mxEvent
|
||||
* @returns An event object, or `null` if the event was not a message event.
|
||||
*/
|
||||
public static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
||||
private static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
||||
const eventId = mxEvent.getId();
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const sender = mxEvent.sender;
|
||||
@@ -140,8 +138,6 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
}
|
||||
|
||||
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
|
||||
private _userInfoRenderer?: UserInfoRenderFunction;
|
||||
private _messageProfileRenderer?: MessageProfileRenderFunction;
|
||||
|
||||
/**
|
||||
* Get the custom room preview bar renderer, if any has been registered.
|
||||
@@ -157,25 +153,4 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
|
||||
this._roomPreviewBarRenderer = renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom user info renderer, if any has been registered.
|
||||
*/
|
||||
public get messageProfileRenderer(): MessageProfileRenderFunction | undefined {
|
||||
return this._messageProfileRenderer;
|
||||
}
|
||||
|
||||
public registerMessageProfile(renderer: MessageProfileRenderFunction): void {
|
||||
this._messageProfileRenderer = renderer;
|
||||
}
|
||||
/**
|
||||
* Get the custom user info renderer, if any has been registered.
|
||||
*/
|
||||
public get userInfoRenderer(): UserInfoRenderFunction | undefined {
|
||||
return this._userInfoRenderer;
|
||||
}
|
||||
|
||||
public registerUserInfo(renderer: UserInfoRenderFunction): void {
|
||||
this._userInfoRenderer = renderer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
|
||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
|
||||
import SpaceStore from "../spaces/SpaceStore";
|
||||
import { type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||
import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter";
|
||||
import { UnreadFilter } from "./skip-list/filters/UnreadFilter";
|
||||
import { PeopleFilter } from "./skip-list/filters/PeopleFilter";
|
||||
@@ -56,16 +56,6 @@ export enum RoomListStoreV3Event {
|
||||
ListsLoaded = "lists_loaded",
|
||||
}
|
||||
|
||||
// The result object for returning rooms from the store
|
||||
export type RoomsResult = {
|
||||
// The ID of the active space queried
|
||||
spaceId: SpaceKey;
|
||||
// The filter queried
|
||||
filterKeys?: FilterKey[];
|
||||
// The resulting list of rooms
|
||||
rooms: Room[];
|
||||
};
|
||||
|
||||
export const LISTS_UPDATE_EVENT = RoomListStoreV3Event.ListsUpdate;
|
||||
export const LISTS_LOADED_EVENT = RoomListStoreV3Event.ListsLoaded;
|
||||
/**
|
||||
@@ -117,15 +107,9 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
|
||||
* @param filterKeys Optional array of filters that the rooms must match against.
|
||||
*/
|
||||
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult {
|
||||
const spaceId = SpaceStore.instance.activeSpace;
|
||||
if (this.roomSkipList?.initialized)
|
||||
return {
|
||||
spaceId: spaceId,
|
||||
filterKeys,
|
||||
rooms: Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys)),
|
||||
};
|
||||
else return { spaceId: spaceId, filterKeys, rooms: [] };
|
||||
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): Room[] {
|
||||
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys));
|
||||
else return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,14 +58,14 @@ export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string):
|
||||
throw friendlyError("Invalid file: too short", _t("encryption|import_invalid_keyfile", { brand }));
|
||||
}
|
||||
|
||||
const salt = body.subarray(1, 1 + 16);
|
||||
const iv = body.subarray(17, 17 + 16);
|
||||
const salt = body.subarray(1, 1 + 16).slice();
|
||||
const iv = body.subarray(17, 17 + 16).slice();
|
||||
const iterations = (body[33] << 24) | (body[34] << 16) | (body[35] << 8) | body[36];
|
||||
const ciphertext = body.subarray(37, 37 + ciphertextLength);
|
||||
const hmac = body.subarray(-32);
|
||||
const ciphertext = body.subarray(37, 37 + ciphertextLength).slice();
|
||||
const hmac = body.subarray(-32).slice();
|
||||
|
||||
const [aesKey, hmacKey] = await deriveKeys(salt, iterations, password);
|
||||
const toVerify = body.subarray(0, -32);
|
||||
const toVerify = body.subarray(0, -32).slice();
|
||||
|
||||
let isValid;
|
||||
try {
|
||||
@@ -180,7 +180,11 @@ export async function encryptMegolmKeyFile(
|
||||
* @param {String} password password
|
||||
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
||||
*/
|
||||
async function deriveKeys(salt: Uint8Array, iterations: number, password: string): Promise<[CryptoKey, CryptoKey]> {
|
||||
async function deriveKeys(
|
||||
salt: Uint8Array<ArrayBuffer>,
|
||||
iterations: number,
|
||||
password: string,
|
||||
): Promise<[CryptoKey, CryptoKey]> {
|
||||
const start = new Date();
|
||||
|
||||
let key;
|
||||
|
||||
@@ -16,7 +16,6 @@ import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||
import ConfirmUserActionDialog from "../components/views/dialogs/ConfirmUserActionDialog";
|
||||
import { openInviteProgressDialog } from "../components/views/dialogs/InviteProgressDialog.tsx";
|
||||
|
||||
export enum InviteState {
|
||||
Invited = "invited",
|
||||
@@ -45,12 +44,6 @@ const USER_BANNED = "IO.ELEMENT.BANNED";
|
||||
export interface MultiInviterOptions {
|
||||
/** Optional callback, fired after each invite */
|
||||
progressCallback?: () => void;
|
||||
|
||||
/**
|
||||
* By default, we will pop up a "Preparing invitations..." dialog while the invites are being sent. Set this to
|
||||
* `true` to inhibit it (in which case, you probably want to implement another bit of feedback UI).
|
||||
*/
|
||||
inhibitProgressDialog?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,59 +88,49 @@ export default class MultiInviter {
|
||||
this.addresses.push(...addresses);
|
||||
this.reason = reason;
|
||||
|
||||
let closeDialog: (() => void) | undefined;
|
||||
if (!this.options.inhibitProgressDialog) {
|
||||
closeDialog = openInviteProgressDialog();
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: "M_INVALID",
|
||||
errorText: _t("invite|invalid_address"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: "M_INVALID",
|
||||
errorText: _t("invite|invalid_address"),
|
||||
};
|
||||
}
|
||||
for (const addr of this.addresses) {
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const addr of this.addresses) {
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.doInvite(addr, false);
|
||||
|
||||
if (this._fatal) {
|
||||
// `doInvite` suffered a fatal error. The error should have been recorded in `errors`; it's up
|
||||
// to the caller to report back to the user.
|
||||
return this.completionStates;
|
||||
}
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.keys(this.errors).length > 0) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter((a) =>
|
||||
UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode),
|
||||
);
|
||||
await this.doInvite(addr, false);
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
await this.handleUnknownProfileUsers(unknownProfileUsers);
|
||||
}
|
||||
if (this._fatal) {
|
||||
// `doInvite` suffered a fatal error. The error should have been recorded in `errors`; it's up
|
||||
// to the caller to report back to the user.
|
||||
return this.completionStates;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(this.errors).length > 0) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter((a) =>
|
||||
UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode),
|
||||
);
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
await this.handleUnknownProfileUsers(unknownProfileUsers);
|
||||
}
|
||||
} finally {
|
||||
// Remember to close the progress dialog, if we opened one.
|
||||
closeDialog?.();
|
||||
}
|
||||
|
||||
return this.completionStates;
|
||||
|
||||
@@ -16,9 +16,8 @@ import { _t } from "../languageHandler";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import SpaceStore from "../stores/spaces/SpaceStore";
|
||||
import Spinner from "../components/views/elements/Spinner";
|
||||
import type { MultiInviterOptions } from "./MultiInviter";
|
||||
|
||||
export interface RoomUpgradeProgress {
|
||||
interface IProgress {
|
||||
roomUpgraded: boolean;
|
||||
roomSynced?: boolean;
|
||||
inviteUsersProgress?: number;
|
||||
@@ -51,8 +50,7 @@ export async function upgradeRoom(
|
||||
handleError = true,
|
||||
updateSpaces = true,
|
||||
awaitRoom = false,
|
||||
progressCallback?: (progress: RoomUpgradeProgress) => void,
|
||||
inhibitInviteProgressDialog = false,
|
||||
progressCallback?: (progress: IProgress) => void,
|
||||
): Promise<string> {
|
||||
const cli = room.client;
|
||||
let spinnerModal: IHandle<any> | undefined;
|
||||
@@ -79,7 +77,7 @@ export async function upgradeRoom(
|
||||
) as Room[];
|
||||
}
|
||||
|
||||
const progress: RoomUpgradeProgress = {
|
||||
const progress: IProgress = {
|
||||
roomUpgraded: false,
|
||||
roomSynced: awaitRoom || inviteUsers ? false : undefined,
|
||||
inviteUsersProgress: inviteUsers ? 0 : undefined,
|
||||
@@ -114,12 +112,9 @@ export async function upgradeRoom(
|
||||
|
||||
if (toInvite.length > 0) {
|
||||
// Errors are handled internally to this function
|
||||
await inviteUsersToRoom(cli, newRoomId, toInvite, {
|
||||
progressCallback: () => {
|
||||
progress.inviteUsersProgress!++;
|
||||
progressCallback?.(progress);
|
||||
},
|
||||
inhibitProgressDialog: inhibitInviteProgressDialog,
|
||||
await inviteUsersToRoom(cli, newRoomId, toInvite, () => {
|
||||
progress.inviteUsersProgress!++;
|
||||
progressCallback?.(progress);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,9 +150,9 @@ async function inviteUsersToRoom(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
userIds: string[],
|
||||
inviteOptions: MultiInviterOptions,
|
||||
progressCallback?: () => void,
|
||||
): Promise<void> {
|
||||
const result = await inviteMultipleToRoom(client, roomId, userIds, inviteOptions);
|
||||
const result = await inviteMultipleToRoom(client, roomId, userIds, { progressCallback });
|
||||
const room = client.getRoom(roomId)!;
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
}
|
||||
|
||||
@@ -297,8 +297,8 @@ export class GroupedArray<K, T> {
|
||||
}
|
||||
}
|
||||
|
||||
export const concat = (...arrays: Uint8Array[]): Uint8Array => {
|
||||
return arrays.reduce((concatenatedSoFar: Uint8Array, toBeConcatenated: Uint8Array) => {
|
||||
export const concat = (...arrays: Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer> => {
|
||||
return arrays.reduce((concatenatedSoFar: Uint8Array<ArrayBuffer>, toBeConcatenated: Uint8Array<ArrayBuffer>) => {
|
||||
const concatenated = new Uint8Array(concatenatedSoFar.length + toBeConcatenated.length);
|
||||
concatenated.set(concatenatedSoFar, 0);
|
||||
concatenated.set(toBeConcatenated, concatenatedSoFar.length);
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface EncryptedPickleKey {
|
||||
* @param {string} deviceId The device ID which owns the pickle key.
|
||||
* @return {Uint8Array} The additional data as a Uint8Array.
|
||||
*/
|
||||
export function getPickleAdditionalData(userId: string, deviceId: string): Uint8Array {
|
||||
export function getPickleAdditionalData(userId: string, deviceId: string): Uint8Array<ArrayBuffer> {
|
||||
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
|
||||
for (let i = 0; i < userId.length; i++) {
|
||||
additionalData[i] = userId.charCodeAt(i);
|
||||
@@ -70,7 +70,7 @@ export function getPickleAdditionalData(userId: string, deviceId: string): Uint8
|
||||
* @returns Data object ready for storing in indexeddb.
|
||||
*/
|
||||
export async function encryptPickleKey(
|
||||
pickleKey: Uint8Array,
|
||||
pickleKey: Uint8Array<ArrayBuffer>,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<EncryptedPickleKey | undefined> {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const HAS_REFRESH_TOKEN_STORAGE_KEY = "mx_has_refresh_token";
|
||||
* @param pickleKey
|
||||
* @returns AES key
|
||||
*/
|
||||
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
||||
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array<ArrayBuffer>> {
|
||||
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
|
||||
for (let i = 0; i < pickleKey.length; i++) {
|
||||
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
|
||||
|
||||
@@ -77,9 +77,6 @@ export class AudioPlayerViewModel
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props, AudioPlayerViewModel.computeSnapshot(props.playback, props.mediaName));
|
||||
this.disposables.trackListener(props.playback, UPDATE_EVENT, this.setSnapshot);
|
||||
// There is no unsubscribe method in SimpleObservable
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.setSnapshot);
|
||||
|
||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||
// is done, and it's not meant to take long anyhow.
|
||||
@@ -100,6 +97,15 @@ export class AudioPlayerViewModel
|
||||
}
|
||||
}
|
||||
|
||||
protected addDownstreamSubscription(): void {
|
||||
this.props.playback.on(UPDATE_EVENT, this.setSnapshot);
|
||||
// There is no unsubscribe method in SimpleObservable
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.setSnapshot);
|
||||
}
|
||||
protected removeDownstreamSubscription(): void {
|
||||
this.props.playback.off(UPDATE_EVENT, this.setSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the snapshot and emits an update to subscribers.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ViewModel } from "../../shared-components/ViewModel";
|
||||
import { Disposables } from "./Disposables";
|
||||
import { Snapshot } from "./Snapshot";
|
||||
import { ViewModelSubscriptions } from "./ViewModelSubscriptions";
|
||||
|
||||
@@ -14,11 +13,13 @@ export abstract class BaseViewModel<T, P> implements ViewModel<T> {
|
||||
protected subs: ViewModelSubscriptions;
|
||||
protected snapshot: Snapshot<T>;
|
||||
protected props: P;
|
||||
protected disposables = new Disposables();
|
||||
|
||||
protected constructor(props: P, initialSnapshot: T) {
|
||||
this.props = props;
|
||||
this.subs = new ViewModelSubscriptions();
|
||||
this.subs = new ViewModelSubscriptions(
|
||||
this.addDownstreamSubscriptionWrapper,
|
||||
this.removeDownstreamSubscriptionWrapper,
|
||||
);
|
||||
this.snapshot = new Snapshot(initialSnapshot, () => {
|
||||
this.subs.emit();
|
||||
});
|
||||
@@ -28,24 +29,37 @@ export abstract class BaseViewModel<T, P> implements ViewModel<T> {
|
||||
return this.subs.add(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around the abstract subscribe callback as we can't assume that the subclassed method
|
||||
* has a bound `this` context.
|
||||
*/
|
||||
private addDownstreamSubscriptionWrapper = (): void => {
|
||||
this.addDownstreamSubscription();
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around the abstract unsubscribe callback as we can't call pass an abstract method directly
|
||||
* in the constructor.
|
||||
*/
|
||||
private removeDownstreamSubscriptionWrapper = (): void => {
|
||||
this.removeDownstreamSubscription();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the first listener subscribes: the subclass should set up any necessary subscriptions
|
||||
* to call this.subs.emit() when the snapshot changes.
|
||||
*/
|
||||
protected abstract addDownstreamSubscription(): void;
|
||||
|
||||
/**
|
||||
* Called when the last listener unsubscribes: the subclass should clean up any subscriptions.
|
||||
*/
|
||||
protected abstract removeDownstreamSubscription(): void;
|
||||
|
||||
/**
|
||||
* Returns the current snapshot of the view model.
|
||||
*/
|
||||
public getSnapshot = (): T => {
|
||||
return this.snapshot.current;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relinquish any resources held by this view-model.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this view-model has been disposed.
|
||||
*/
|
||||
public get isDisposed(): boolean {
|
||||
return this.disposables.isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* Something that needs to be eventually disposed. This can be:
|
||||
* - A function that does the disposing
|
||||
* - An object containing a dispose method which does the disposing
|
||||
*/
|
||||
export type DisposableItem = { dispose: () => void } | (() => void);
|
||||
|
||||
/**
|
||||
* This class provides a way for the view-model to track any resource
|
||||
* that it needs to eventually relinquish.
|
||||
*/
|
||||
export class Disposables {
|
||||
private readonly disposables: DisposableItem[] = [];
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
/**
|
||||
* Relinquish all tracked disposable values
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) return;
|
||||
this._isDisposed = true;
|
||||
for (const disposable of this.disposables) {
|
||||
if (typeof disposable === "function") {
|
||||
disposable();
|
||||
} else {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a value that needs to be eventually relinquished
|
||||
*/
|
||||
public track<T extends DisposableItem>(disposable: T): T {
|
||||
this.throwIfDisposed();
|
||||
this.disposables.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener that will be removed on dispose
|
||||
*/
|
||||
public trackListener(emitter: EventEmitter, event: string, callback: (...args: unknown[]) => void): void {
|
||||
this.throwIfDisposed();
|
||||
emitter.on(event, callback);
|
||||
this.track(() => {
|
||||
emitter.off(event, callback);
|
||||
});
|
||||
}
|
||||
|
||||
private throwIfDisposed(): void {
|
||||
if (this.isDisposed) throw new Error("Disposable is already disposed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this disposable has been disposed
|
||||
*/
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,20 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility class for view models to manage subscriptions to their updates
|
||||
* Utility class for view models to manage suscriptions to their updates
|
||||
*/
|
||||
export class ViewModelSubscriptions {
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
* @param subscribeCallback Called when the first listener subscribes.
|
||||
* @param unsubscribeCallback Called when the last listener unsubscribes.
|
||||
*/
|
||||
public constructor(
|
||||
private subscribeCallback: () => void,
|
||||
private unsubscribeCallback: () => void,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in the view model.
|
||||
* @param listener Will be called whenever the snapshot changes.
|
||||
@@ -18,8 +27,15 @@ export class ViewModelSubscriptions {
|
||||
*/
|
||||
public add = (listener: () => void): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
if (this.listeners.size === 1) {
|
||||
this.subscribeCallback();
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0) {
|
||||
this.unsubscribeCallback();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,18 @@ export class TextualEventViewModel extends BaseViewModel<TextualEventViewSnapsho
|
||||
public constructor(props: EventTileTypeProps) {
|
||||
super(props, { content: "" });
|
||||
this.setTextFromEvent();
|
||||
this.disposables.trackListener(this.props.mxEvent, MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
}
|
||||
|
||||
private setTextFromEvent = (): void => {
|
||||
const content = textForEvent(this.props.mxEvent, MatrixClientPeg.safeGet(), true, this.props.showHiddenEvents);
|
||||
this.snapshot.set({ content });
|
||||
};
|
||||
|
||||
protected addDownstreamSubscription = (): void => {
|
||||
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
|
||||
protected removeDownstreamSubscription = (): void => {
|
||||
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ global.URL.revokeObjectURL = jest.fn();
|
||||
|
||||
// polyfilling TextEncoder as it is not available on JSDOM
|
||||
// view https://github.com/facebook/jest/issues/9983
|
||||
global.TextEncoder = TextEncoder;
|
||||
// XXX: Node's implementation has marginally different types, so we fudge it
|
||||
(globalThis as any).TextEncoder = TextEncoder;
|
||||
// @ts-ignore
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ describe("VoiceMessageRecording", () => {
|
||||
const encryptedFile = {} as unknown as EncryptedFile;
|
||||
|
||||
beforeEach(() => {
|
||||
voiceRecording.onDataAvailable!(testBuf);
|
||||
voiceRecording.onDataAvailable!(testBuf.buffer);
|
||||
});
|
||||
|
||||
it("contentLength should return the buffer length", () => {
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("useRoomListHeaderViewModel", () => {
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
const fn = jest
|
||||
.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
|
||||
.mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
|
||||
.mockImplementation(() => [...rooms]);
|
||||
return { rooms, fn };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("RoomListViewModel", () => {
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
const fn = jest
|
||||
.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
|
||||
.mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
|
||||
.mockImplementation(() => [...rooms]);
|
||||
return { rooms, fn };
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ describe("RoomListViewModel", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
expect(vm.current.roomsResult.rooms).toHaveLength(10);
|
||||
expect(vm.current.rooms).toHaveLength(10);
|
||||
for (const room of rooms) {
|
||||
expect(vm.current.roomsResult.rooms).toContain(room);
|
||||
expect(vm.current.rooms).toContain(room);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("RoomListViewModel", () => {
|
||||
await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vm.current.roomsResult.rooms).toContain(newRoom);
|
||||
expect(vm.current.rooms).toContain(newRoom);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ describe("RoomListViewModel", () => {
|
||||
describe("Sticky room and active index", () => {
|
||||
function expectActiveRoom(vm: ReturnType<typeof useRoomListViewModel>, i: number, roomId: string) {
|
||||
expect(vm.activeIndex).toEqual(i);
|
||||
expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId);
|
||||
expect(vm.rooms[i].roomId).toEqual(roomId);
|
||||
}
|
||||
|
||||
it("active index is calculated with the last opened room in a space", () => {
|
||||
@@ -187,9 +187,9 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
// Let's say all the rooms are in space1
|
||||
const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] };
|
||||
const roomsInSpace1 = [...rooms];
|
||||
// Let's say all rooms with even index are in space 2
|
||||
const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) };
|
||||
const roomsInSpace2 = [...rooms].filter((_, i) => i % 2 === 0);
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() =>
|
||||
currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2,
|
||||
);
|
||||
|
||||
@@ -137,7 +137,6 @@ describe("InviteDialog", () => {
|
||||
supportsThreads: jest.fn().mockReturnValue(false),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
getClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||
invite: jest.fn(),
|
||||
});
|
||||
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
@@ -407,18 +406,6 @@ describe("InviteDialog", () => {
|
||||
expect(tile).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("while the invite is in progress", () => {
|
||||
it("should show a spinner", async () => {
|
||||
mockClient.invite.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
await enterIntoSearchField(bobId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Invite" }));
|
||||
|
||||
await screen.findByText("Preparing invitations...");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when inviting a user with an unknown profile", () => {
|
||||
beforeEach(async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import InviteProgressBody from "../../../../../src/components/views/dialogs/InviteProgressBody.tsx";
|
||||
|
||||
describe("InviteProgressBody", () => {
|
||||
it("should match snapshot", () => {
|
||||
const { asFragment } = render(<InviteProgressBody />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InviteProgressBody should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_InviteProgressBody"
|
||||
>
|
||||
<div
|
||||
class="mx_InlineSpinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<h1>
|
||||
Preparing invitations...
|
||||
</h1>
|
||||
Do not close the app until finished.
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -19,7 +19,7 @@ describe("<EmptyRoomList />", () => {
|
||||
beforeEach(() => {
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
|
||||
@@ -9,25 +9,28 @@ import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { fireEvent } from "@testing-library/dom";
|
||||
import { VirtuosoMockContext } from "react-virtuoso";
|
||||
|
||||
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
|
||||
describe("<RoomList />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
// Needed to render the virtualized list in rtl tests
|
||||
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
matrixClient = stubClient();
|
||||
const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`));
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms },
|
||||
rooms,
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
@@ -41,18 +44,7 @@ describe("<RoomList />", () => {
|
||||
});
|
||||
|
||||
it("should render a room list", () => {
|
||||
const { asFragment } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
// At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]".
|
||||
// This is a general issue with the react-virtuoso library.
|
||||
// TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281
|
||||
const { asFragment } = render(<RoomList vm={vm} />, withClientContextRenderOptions(matrixClient));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -61,15 +53,7 @@ describe("<RoomList />", () => {
|
||||
{ shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" },
|
||||
])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => {
|
||||
const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue();
|
||||
const { getByTestId } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
const { getByTestId } = render(<RoomList vm={vm} />, withClientContextRenderOptions(matrixClient));
|
||||
const roomList = getByTestId("room-list");
|
||||
fireEvent.keyDown(roomList, shortcut);
|
||||
|
||||
|
||||
@@ -28,20 +28,6 @@ describe("<RoomListItemView />", () => {
|
||||
let defaultValue: RoomListItemViewState;
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderRoomListItem = (props: Partial<React.ComponentProps<typeof RoomListItemView>> = {}) => {
|
||||
const defaultProps = {
|
||||
room,
|
||||
isSelected: false,
|
||||
isFocused: false,
|
||||
onFocus: jest.fn(),
|
||||
roomIndex: 0,
|
||||
roomCount: 1,
|
||||
};
|
||||
|
||||
return render(<RoomListItemView {...defaultProps} {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkRoom(matrixClient, "room1");
|
||||
@@ -74,10 +60,7 @@ describe("<RoomListItemView />", () => {
|
||||
|
||||
test("should render a room item", () => {
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
roomCount: 0,
|
||||
});
|
||||
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} isSelected={false} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -85,17 +68,15 @@ describe("<RoomListItemView />", () => {
|
||||
defaultValue.messagePreview = "The message looks like this";
|
||||
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
});
|
||||
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} isSelected={false} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should call openRoom when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoomListItem();
|
||||
render(<RoomListItemView room={room} isSelected={false} />);
|
||||
|
||||
await user.click(screen.getByRole("option", { name: `Open room ${room.name}` }));
|
||||
await user.click(screen.getByRole("button", { name: `Open room ${room.name}` }));
|
||||
expect(defaultValue.openRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -103,9 +84,8 @@ describe("<RoomListItemView />", () => {
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true });
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderRoomListItem();
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
expect(screen.queryByRole("button", { name: "More Options" })).toBeNull();
|
||||
|
||||
await user.hover(listItem);
|
||||
@@ -113,33 +93,19 @@ describe("<RoomListItemView />", () => {
|
||||
});
|
||||
|
||||
test("should hover decoration if focused", async () => {
|
||||
const { rerender } = renderRoomListItem({
|
||||
isFocused: true,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.click(listItem);
|
||||
expect(listItem).toHaveClass("mx_RoomListItemView_hover");
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
expect(listItem).toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover");
|
||||
|
||||
rerender(
|
||||
<RoomListItemView
|
||||
room={room}
|
||||
isSelected={false}
|
||||
isFocused={false}
|
||||
onFocus={jest.fn()}
|
||||
roomIndex={0}
|
||||
roomCount={1}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(listItem).not.toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover"));
|
||||
await user.tab();
|
||||
await waitFor(() => expect(listItem).not.toHaveClass("mx_RoomListItemView_hover"));
|
||||
});
|
||||
|
||||
test("should be selected if isSelected=true", async () => {
|
||||
const { asFragment } = renderRoomListItem({
|
||||
isSelected: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute(
|
||||
const { asFragment } = render(<RoomListItemView room={room} isSelected={true} />);
|
||||
expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
@@ -152,8 +118,7 @@ describe("<RoomListItemView />", () => {
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
const { asFragment } = renderRoomListItem();
|
||||
|
||||
const { asFragment } = render(<RoomListItemView room={room} isSelected={false} />);
|
||||
expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
@@ -166,9 +131,8 @@ describe("<RoomListItemView />", () => {
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />);
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.hover(listItem);
|
||||
|
||||
expect(screen.queryByRole("notification-decoration")).toBeNull();
|
||||
@@ -182,9 +146,8 @@ describe("<RoomListItemView />", () => {
|
||||
showContextMenu: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const button = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const button = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]);
|
||||
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
||||
// Menu should close
|
||||
|
||||
@@ -23,7 +23,7 @@ jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewMode
|
||||
describe("<RoomListView />", () => {
|
||||
const defaultValue: RoomListViewState = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
@@ -56,10 +56,10 @@ describe("<RoomListView />", () => {
|
||||
it("should render a room list", () => {
|
||||
mocked(useRoomListViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] },
|
||||
rooms: [mkRoom(matrixClient, "testing room")],
|
||||
});
|
||||
|
||||
render(<RoomListView />);
|
||||
expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("grid", { name: "Room list" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,556 +3,531 @@
|
||||
exports[`<RoomList /> should render a room list 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="Room list"
|
||||
context="[object Object]"
|
||||
class="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
data-virtuoso-scroller="true"
|
||||
role="listbox"
|
||||
style="height: 100%; outline: none; overflow-y: auto; position: relative;"
|
||||
>
|
||||
<div
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
style="overflow: visible; height: 0px; width: 0px;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 0px; padding-top: 0px;"
|
||||
aria-label="Room list"
|
||||
aria-readonly="true"
|
||||
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
|
||||
role="grid"
|
||||
style="box-sizing: border-box; direction: ltr; height: 1500px; position: relative; width: 1500px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
data-index="0"
|
||||
data-item-index="0"
|
||||
data-known-size="48"
|
||||
class="ReactVirtualized__Grid__innerScrollContainer"
|
||||
role="row"
|
||||
style="width: auto; height: 480px; max-width: 1500px; max-height: 480px; overflow: hidden; position: relative;"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room0"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 0px; width: 100%;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room0"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room0
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room0"
|
||||
>
|
||||
room0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="1"
|
||||
data-item-index="1"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="2"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 48px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="2"
|
||||
data-item-index="2"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room2"
|
||||
aria-posinset="3"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 96px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room2"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room2
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room2"
|
||||
>
|
||||
room2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="3"
|
||||
data-item-index="3"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room3"
|
||||
aria-posinset="4"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 144px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room3"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room3
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room3"
|
||||
>
|
||||
room3
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="4"
|
||||
data-item-index="4"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room4"
|
||||
aria-posinset="5"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 192px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room4"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room4
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room4"
|
||||
>
|
||||
room4
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="5"
|
||||
data-item-index="5"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room5"
|
||||
aria-posinset="6"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 240px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room5"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room5
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room5"
|
||||
>
|
||||
room5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="6"
|
||||
data-item-index="6"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room6"
|
||||
aria-posinset="7"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 288px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room6"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room6
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room6"
|
||||
>
|
||||
room6
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="7"
|
||||
data-item-index="7"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room7"
|
||||
aria-posinset="8"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 336px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room7"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room7
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room7"
|
||||
>
|
||||
room7
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="8"
|
||||
data-item-index="8"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room8"
|
||||
aria-posinset="9"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 384px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room8"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room8
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room8"
|
||||
>
|
||||
room8
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="9"
|
||||
data-item-index="9"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room9"
|
||||
aria-posinset="10"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 432px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room9"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room9
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room9"
|
||||
>
|
||||
room9
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,6 +535,20 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-triggers"
|
||||
>
|
||||
<div
|
||||
class="expand-trigger"
|
||||
>
|
||||
<div
|
||||
style="width: 1501px; height: 1501px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="contract-trigger"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -38,43 +38,41 @@ exports[`<RoomListItemMenuView /> should render the more options menu 1`] = `
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«r9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«r7»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«r9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«r7»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -117,43 +115,41 @@ exports[`<RoomListItemMenuView /> should render the notification options menu 1`
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«rp»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«rn»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«rp»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«rn»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -4,46 +4,47 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="true"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView mx_RoomListItemView_selected"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView mx_RoomListItemView_selected"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,59 +56,60 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_unread-counter_9mg0k_8"
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span
|
||||
class="_unread-counter_9mg0k_8"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -118,46 +120,47 @@ exports[`<RoomListItemView /> should render a room item 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="0"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,52 +172,53 @@ exports[`<RoomListItemView /> should render a room item with a message preview 1
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListItemView_messagePreview"
|
||||
title="The message looks like this"
|
||||
>
|
||||
The message looks like this
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListItemView_messagePreview"
|
||||
title="The message looks like this"
|
||||
>
|
||||
The message looks like this
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { act, fireEvent, screen, waitFor } from "jest-matrix-react";
|
||||
import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type JSX } from "react";
|
||||
|
||||
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
|
||||
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
@@ -20,6 +21,14 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||
jest.mock("react-virtualized", () => {
|
||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||
return {
|
||||
...ReactVirtualized,
|
||||
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
|
||||
};
|
||||
});
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { act } from "react";
|
||||
import { waitFor, fireEvent } from "jest-matrix-react";
|
||||
import { type Room, type RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type JSX } from "react";
|
||||
|
||||
import { filterConsole } from "../../../../../test-utils";
|
||||
import { type Rendered, renderMemberList } from "./common";
|
||||
@@ -18,6 +19,14 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||
jest.mock("react-virtualized", () => {
|
||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||
return {
|
||||
...ReactVirtualized,
|
||||
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
|
||||
};
|
||||
});
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function renderMemberList(
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
{children}
|
||||
</VirtuosoMockContext.Provider>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("ListView", () => {
|
||||
return render(getListViewComponent(mergedProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 400, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
{children}
|
||||
</VirtuosoMockContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -460,7 +460,7 @@ describe("RoomListStoreV3", () => {
|
||||
store.on(LISTS_UPDATE_EVENT, fn);
|
||||
|
||||
// The rooms which belong to the space should not be shown
|
||||
const result = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
|
||||
const result = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
|
||||
for (const id of roomIds) {
|
||||
expect(result).not.toContain(id);
|
||||
}
|
||||
@@ -469,7 +469,7 @@ describe("RoomListStoreV3", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
|
||||
SpaceStore.instance.emit(UPDATE_SELECTED_SPACE);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
const result2 = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
|
||||
const result2 = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
|
||||
for (const id of roomIds) {
|
||||
expect(result2).toContain(id);
|
||||
}
|
||||
@@ -492,7 +492,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Sorted, filtered rooms should be 8, 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]);
|
||||
expect(result).toHaveLength(3);
|
||||
for (const i of [8, 27, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -527,7 +527,7 @@ describe("RoomListStoreV3", () => {
|
||||
expect(fn).toHaveBeenCalled();
|
||||
|
||||
// Sorted, filtered rooms should be 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -552,7 +552,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -569,7 +569,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Since there's no unread yet, we expect zero results
|
||||
let result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
let result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]);
|
||||
expect(result).toHaveLength(0);
|
||||
|
||||
// Mock so that room at index 8 is marked as unread
|
||||
@@ -584,7 +584,7 @@ describe("RoomListStoreV3", () => {
|
||||
);
|
||||
|
||||
// Now we expect room at index 8 to show as unread
|
||||
result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain(rooms[8]);
|
||||
});
|
||||
@@ -607,14 +607,14 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const peopleRooms = store.getSortedRoomsInActiveSpace([FilterKey.PeopleFilter]).rooms;
|
||||
const peopleRooms = store.getSortedRoomsInActiveSpace([FilterKey.PeopleFilter]);
|
||||
expect(peopleRooms).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(peopleRooms).toContain(rooms[i]);
|
||||
}
|
||||
|
||||
// Rest are normal rooms
|
||||
const nonDms = store.getSortedRoomsInActiveSpace([FilterKey.RoomsFilter]).rooms;
|
||||
const nonDms = store.getSortedRoomsInActiveSpace([FilterKey.RoomsFilter]);
|
||||
expect(nonDms).toHaveLength(3);
|
||||
for (const i of [6, 13, 75]) {
|
||||
expect(nonDms).toContain(rooms[i]);
|
||||
@@ -638,7 +638,7 @@ describe("RoomListStoreV3", () => {
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]);
|
||||
expect(result).toHaveLength(5);
|
||||
for (const room of invitedRooms) {
|
||||
expect(result).toContain(room);
|
||||
@@ -663,7 +663,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.MentionsFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.MentionsFilter]);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -685,7 +685,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Sorted, filtered rooms should be 8, 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.LowPriorityFilter]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.LowPriorityFilter]);
|
||||
expect(result).toHaveLength(3);
|
||||
for (const i of [8, 27, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -713,10 +713,7 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should give us only room at 8 since that's the only room which matches both filters
|
||||
const result = store.getSortedRoomsInActiveSpace([
|
||||
FilterKey.UnreadFilter,
|
||||
FilterKey.FavouriteFilter,
|
||||
]).rooms;
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter, FilterKey.FavouriteFilter]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain(rooms[8]);
|
||||
});
|
||||
|
||||
@@ -59,8 +59,8 @@ const TEST_VECTORS = [
|
||||
],
|
||||
];
|
||||
|
||||
function stringToArray(s: string): ArrayBufferLike {
|
||||
return new TextEncoder().encode(s).buffer;
|
||||
function stringToArray(s: string): ArrayBuffer {
|
||||
return new TextEncoder().encode(s).buffer as ArrayBuffer;
|
||||
}
|
||||
|
||||
describe("MegolmExportEncryption", function () {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Modal, { type ComponentType, type ComponentProps } from "../../../src/Mod
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import MultiInviter, { type CompletionStates } from "../../../src/utils/MultiInviter";
|
||||
import * as TestUtilsMatrix from "../../test-utils";
|
||||
import AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import type AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import ConfirmUserActionDialog from "../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
||||
|
||||
const ROOMID = "!room:server";
|
||||
@@ -24,14 +24,10 @@ const MXID1 = "@user1:server";
|
||||
const MXID2 = "@user2:server";
|
||||
const MXID3 = "@user3:server";
|
||||
|
||||
const MXID_PROFILE_STATES: Record<string, () => {}> = {
|
||||
[MXID1]: () => ({}),
|
||||
[MXID2]: () => {
|
||||
throw new MatrixError({ errcode: "M_FORBIDDEN" });
|
||||
},
|
||||
[MXID3]: () => {
|
||||
throw new MatrixError({ errcode: "M_NOT_FOUND" });
|
||||
},
|
||||
const MXID_PROFILE_STATES: Record<string, Promise<any>> = {
|
||||
[MXID1]: Promise.resolve({}),
|
||||
[MXID2]: Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN" })),
|
||||
[MXID3]: Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
};
|
||||
|
||||
jest.mock("../../../src/Modal", () => ({
|
||||
@@ -55,12 +51,11 @@ const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
||||
};
|
||||
|
||||
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
||||
mocked(Modal.createDialog).mockImplementation((Element: ComponentType, props?: ComponentProps<ComponentType>) => {
|
||||
if (Element === AskInviteAnywayDialog) {
|
||||
mocked(Modal.createDialog).mockImplementation(
|
||||
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
||||
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
||||
}
|
||||
return { close: jest.fn(), finished: new Promise(() => {}) };
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const expectAllInvitedResult = (result: CompletionStates) => {
|
||||
@@ -77,7 +72,6 @@ describe("MultiInviter", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mocked(Modal.createDialog).mockReturnValue({ close: jest.fn(), finished: new Promise(() => {}) });
|
||||
|
||||
TestUtilsMatrix.stubClient();
|
||||
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
||||
@@ -86,10 +80,8 @@ describe("MultiInviter", () => {
|
||||
client.invite.mockResolvedValue({});
|
||||
|
||||
client.getProfileInfo = jest.fn();
|
||||
client.getProfileInfo.mockImplementation(async (userId: string) => {
|
||||
const m = MXID_PROFILE_STATES[userId];
|
||||
if (m) return m();
|
||||
throw new Error();
|
||||
client.getProfileInfo.mockImplementation((userId: string) => {
|
||||
return MXID_PROFILE_STATES[userId] || Promise.reject();
|
||||
});
|
||||
client.unban = jest.fn();
|
||||
|
||||
@@ -97,22 +89,6 @@ describe("MultiInviter", () => {
|
||||
});
|
||||
|
||||
describe("invite", () => {
|
||||
it("should show a progress dialog while the invite happens", async () => {
|
||||
const mockModalHandle = { close: jest.fn(), finished: new Promise<[]>(() => {}) };
|
||||
mocked(Modal.createDialog).mockReturnValue(mockModalHandle);
|
||||
|
||||
const invitePromise = Promise.withResolvers<{}>();
|
||||
client.invite.mockReturnValue(invitePromise.promise);
|
||||
|
||||
const resultPromise = inviter.invite([MXID1]);
|
||||
expect(Modal.createDialog).toHaveBeenCalledTimes(1);
|
||||
expect(mockModalHandle.close).not.toHaveBeenCalled();
|
||||
|
||||
invitePromise.resolve({});
|
||||
await resultPromise;
|
||||
expect(mockModalHandle.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
||||
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Disposables } from "../../../src/viewmodels/base/Disposables";
|
||||
|
||||
describe("Disposable", () => {
|
||||
it("isDisposed is true after dispose() is called", () => {
|
||||
const disposables = new Disposables();
|
||||
expect(disposables.isDisposed).toEqual(false);
|
||||
disposables.dispose();
|
||||
expect(disposables.isDisposed).toEqual(true);
|
||||
});
|
||||
|
||||
it("dispose() calls the correct disposing function", () => {
|
||||
const disposables = new Disposables();
|
||||
|
||||
const item1 = {
|
||||
foo: 5,
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
disposables.track(item1);
|
||||
|
||||
const item2 = jest.fn();
|
||||
disposables.track(item2);
|
||||
|
||||
disposables.dispose();
|
||||
|
||||
expect(item1.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(item2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("Throws error if acting on already disposed disposables", () => {
|
||||
const disposables = new Disposables();
|
||||
disposables.dispose();
|
||||
expect(() => {
|
||||
disposables.track(jest.fn);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("Removes tracked event listeners on dispose", () => {
|
||||
const disposables = new Disposables();
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
const fn = jest.fn();
|
||||
disposables.trackListener(emitter, "FooEvent", fn);
|
||||
emitter.emit("FooEvent");
|
||||
expect(fn).toHaveBeenCalled();
|
||||
|
||||
disposables.dispose();
|
||||
expect(emitter.listenerCount("FooEvent", fn)).toEqual(0);
|
||||
});
|
||||
});
|
||||
49
yarn.lock
@@ -1208,7 +1208,7 @@
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
|
||||
"@babel/plugin-transform-typescript" "^7.27.1"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.3.tgz#75c5034b55ba868121668be5d5bb31cc64e6e61a"
|
||||
integrity sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==
|
||||
@@ -4261,6 +4261,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/png-chunks-extract/-/png-chunks-extract-1.0.2.tgz#31dd8d74d6ba879ace317c1e042dcdabc6300d6e"
|
||||
integrity sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||
|
||||
"@types/qrcode@^1.3.5":
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac"
|
||||
@@ -4305,6 +4310,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react-virtualized@^9.21.30":
|
||||
version "9.22.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.22.2.tgz#97674f050a85d0f7aab827b3d894f3f1b237922a"
|
||||
integrity sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@19.1.10":
|
||||
version "19.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.10.tgz#a05015952ef328e1b85579c839a71304b07d21d9"
|
||||
@@ -6193,6 +6206,11 @@ clone-deep@^4.0.1:
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clsx@^1.0.4:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
@@ -7157,7 +7175,7 @@ dom-converter@^0.2.0:
|
||||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
dom-helpers@^5.0.1, dom-helpers@^5.1.3:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
||||
@@ -13170,6 +13188,11 @@ react-is@^17.0.2:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-property@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6"
|
||||
@@ -13230,7 +13253,19 @@ react-transition-group@^4.4.1:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-virtuoso@^4.14.0:
|
||||
react-virtualized@^9.22.5:
|
||||
version "9.22.6"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.6.tgz#3ae2aa69eca61cf3af332e2f9d6b4aa5638786d5"
|
||||
integrity sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.2"
|
||||
clsx "^1.0.4"
|
||||
dom-helpers "^5.1.3"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react-virtuoso@^4.12.6:
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.14.0.tgz#6998631cb0a86efc2b15e551f55e7199a0f25c7a"
|
||||
integrity sha512-fR+eiCvirSNIRvvCD7ueJPRsacGQvUbjkwgWzBZXVq+yWypoH7mRUvWJzGHIdoRaCZCT+6mMMMwIG2S1BW3uwA==
|
||||
@@ -15128,10 +15163,10 @@ typedarray-to-buffer@^3.1.5:
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
typescript@5.9.2:
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
|
||||
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
|
||||
|
||||
ua-parser-js@^1.0.2:
|
||||
version "1.0.40"
|
||||
|
||||