Compare commits
25 Commits
t3chguy/re
...
hs/add-hid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7a185baea | ||
|
|
93009d4613 | ||
|
|
71257d97e7 | ||
|
|
60eeb8a7de | ||
|
|
571a2e373d | ||
|
|
d0b8564660 | ||
|
|
28ea91566a | ||
|
|
ef32747473 | ||
|
|
7696516e8b | ||
|
|
46b1234a1d | ||
|
|
b9c0d63e3e | ||
|
|
cf7e52c6fc | ||
|
|
e87eb127ee | ||
|
|
83e421daf2 | ||
|
|
d6fb24dea7 | ||
|
|
a518c8d662 | ||
|
|
c759e516bd | ||
|
|
c8b55c3dfe | ||
|
|
7197093744 | ||
|
|
4e34adb854 | ||
|
|
72c2a3eb07 | ||
|
|
4d290461c4 | ||
|
|
0cc06450d7 | ||
|
|
9376d71831 | ||
|
|
6d5442a87b |
@@ -11,7 +11,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download release tarball
|
||||
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
|
||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
fileName: element-*.tar.gz*
|
||||
|
||||
11
.github/workflows/build_develop.yml
vendored
@@ -26,6 +26,12 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
||||
- uses: unfor19/install-aws-cli-action@v1
|
||||
with:
|
||||
version: 2.22.35
|
||||
verbose: false
|
||||
arch: amd64
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -109,11 +115,10 @@ jobs:
|
||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||
# as the expires after 24h and requires auth to download.
|
||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||
- name: Deploy to R2
|
||||
run: |
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||
|
||||
4
.github/workflows/docker.yaml
vendored
@@ -37,14 +37,14 @@ jobs:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4
|
||||
uses: guibranco/github-status-action-v2@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
14
package.json
@@ -68,13 +68,13 @@
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||
},
|
||||
"resolutions": {
|
||||
"@playwright/test": "1.51.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.2.0",
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001704",
|
||||
"testcontainers": "10.21.0",
|
||||
"caniuse-lite": "1.0.30001701",
|
||||
"testcontainers": "10.20.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -107,7 +107,6 @@
|
||||
"css-tree": "^3.0.0",
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"domutils": "^3.2.2",
|
||||
"emojibase-regex": "15.3.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -116,12 +115,12 @@
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"js-xxhash": "^4.0.0",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.2.0",
|
||||
"linkify-react": "4.2.0",
|
||||
"linkify-string": "4.2.0",
|
||||
"linkifyjs": "4.2.0",
|
||||
@@ -145,7 +144,6 @@
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
@@ -276,7 +274,7 @@
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.5.3",
|
||||
"prettier": "3.5.2",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
|
||||
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity was reset",
|
||||
"Sender's verified identity has changed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,9 +29,7 @@ test.describe("Key storage out of sync toast", () => {
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// We need to wait for there to be two toasts as the wait below won't work in isolation:
|
||||
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||
// it would always be checking the same toast, even if another one is now the first.
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
|
||||
@@ -221,9 +221,6 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ test.describe("Room list filters and sort", () => {
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
function getPrimaryFilters(page: Page) {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
@@ -25,113 +33,56 @@ test.describe("Room list filters and sort", () => {
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
const unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, favouriteId);
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
const unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, favouriteId);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
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();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Empty room list", () => {
|
||||
/**
|
||||
* Get the empty state
|
||||
* @param page
|
||||
*/
|
||||
function getEmptyRoomList(page: Page) {
|
||||
return page.getByTestId("empty-room-list");
|
||||
}
|
||||
|
||||
test(
|
||||
"should render the default placeholder when there is no filter",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
||||
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
|
||||
|
||||
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
|
||||
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||
test(
|
||||
`should render the placeholder for ${filter} filter`,
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||
},
|
||||
);
|
||||
});
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
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();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,57 +77,4 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
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("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Let's create a new room and invite the bot
|
||||
const room1Id = await app.client.createRoom({
|
||||
name: "Unread Room 1",
|
||||
invite: [bot.credentials?.userId],
|
||||
});
|
||||
await bot.awaitRoomMembership(room1Id);
|
||||
|
||||
// Let's create another room as well
|
||||
const room2Id = await app.client.createRoom({
|
||||
name: "Unread Room 2",
|
||||
invite: [bot.credentials?.userId],
|
||||
});
|
||||
await bot.awaitRoomMembership(room2Id);
|
||||
|
||||
// Let's configure unread room 1 so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(room1Id);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than room 1 or room 2
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both room 1 and room 2
|
||||
await bot.sendMessage(room1Id, "Hello!");
|
||||
await bot.sendMessage(room2Id, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show room 2!!
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 2" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 1" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { selectHomeserver } from "../utils";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { createBot } from "../crypto/utils.ts";
|
||||
|
||||
// This test requires fixed credentials for the device signing keys below to work
|
||||
const username = "user1234";
|
||||
@@ -259,34 +258,6 @@ test.describe("Login", () => {
|
||||
|
||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||
page,
|
||||
homeserver,
|
||||
credentials,
|
||||
}) => {
|
||||
// Create a different device which is cross-signed, meaning we need to verify this device
|
||||
await createBot(page, homeserver, credentials, true);
|
||||
|
||||
// Wait to avoid homeserver rate limit on logins
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Load the page and see that we are asked to verify
|
||||
await page.goto("/#/welcome");
|
||||
await login(page, homeserver, credentials);
|
||||
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
|
||||
// Click "Verify with another device"
|
||||
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||
|
||||
// Cancel the new dialog
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
// Check that we are still being asked to verify
|
||||
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,33 +73,4 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await revokeAccessTokenPromise;
|
||||
await revokeRefreshTokenPromise;
|
||||
});
|
||||
|
||||
test(
|
||||
"it should log out the user & wipe data when logging out via MAS",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ mas, page, mailpitClient }, testInfo) => {
|
||||
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await expect(page.getByText("Welcome")).toBeVisible();
|
||||
await page.goto("about:blank");
|
||||
|
||||
// @ts-expect-error
|
||||
const result = await mas.manage("kill-sessions", userId);
|
||||
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
|
||||
|
||||
await page.goto("http://localhost:8080");
|
||||
await expect(
|
||||
page.getByText("For security, this session has been signed out. Please sign in again."),
|
||||
).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
|
||||
|
||||
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||
expect(localStorageKeys).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -10,7 +10,6 @@ import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const ROOM_NAME_LONG =
|
||||
@@ -134,17 +133,6 @@ test.describe("RightPanel", () => {
|
||||
await page.getByLabel("Room info").nth(1).click();
|
||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||
});
|
||||
test.describe("room reporting", () => {
|
||||
test.skip(isDendrite, "Dendrite does not implement room reporting");
|
||||
test("should handle reporting a room", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report Room" });
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await expect(page.getByText("Your report was sent.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("in spaces", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => {
|
||||
// Click "Show advanced" link button
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
await tab.getByLabel("Use bundled emoji font").click();
|
||||
await tab.getByLabel("Use a system font").click();
|
||||
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
|
||||
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
|
||||
|
||||
// Assert that the font-family value was removed
|
||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||
|
||||
@@ -7,15 +7,47 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page, type Request } from "@playwright/test";
|
||||
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
|
||||
const test = base.extend<{
|
||||
slidingSyncProxy: StartedTestContainer;
|
||||
testRoom: { roomId: string; name: string };
|
||||
joinedBot: Bot;
|
||||
}>({
|
||||
slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => {
|
||||
const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3")
|
||||
.withNetwork(network)
|
||||
.withExposedPorts(8008)
|
||||
.withLogConsumer(logger.getConsumer("sliding-sync-proxy"))
|
||||
.withWaitStrategy(Wait.forHttp("/client/server.json", 8008))
|
||||
.withEnvironment({
|
||||
SYNCV3_SECRET: "bwahahaha",
|
||||
SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`,
|
||||
SYNCV3_SERVER: `http://homeserver:8008`,
|
||||
})
|
||||
.start();
|
||||
|
||||
const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`;
|
||||
await page.addInitScript((proxyAddress) => {
|
||||
window.localStorage.setItem(
|
||||
"mx_local_settings",
|
||||
JSON.stringify({
|
||||
feature_sliding_sync_proxy_url: proxyAddress,
|
||||
}),
|
||||
);
|
||||
window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true");
|
||||
}, proxyAddress);
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
// Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script
|
||||
credentials: async ({ slidingSyncProxy, credentials }, use) => {
|
||||
await use(credentials);
|
||||
},
|
||||
testRoom: async ({ user, app }, use) => {
|
||||
const name = "Test Room";
|
||||
const roomId = await app.client.createRoom({ name });
|
||||
@@ -50,14 +82,6 @@ test.describe("Sliding Sync", () => {
|
||||
});
|
||||
};
|
||||
|
||||
test.use({
|
||||
config: {
|
||||
features: {
|
||||
feature_simplified_sliding_sync: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Load the user fixture for all tests
|
||||
test.beforeEach(({ user }) => {});
|
||||
|
||||
@@ -164,7 +188,15 @@ test.describe("Sliding Sync", () => {
|
||||
).not.toBeAttached();
|
||||
});
|
||||
|
||||
test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
||||
test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
||||
// TODO: for now. Later we should.
|
||||
|
||||
// disable notifs in this room (TODO: CS API call?)
|
||||
const locator = page.getByRole("treeitem", { name: "Test Room" });
|
||||
await locator.hover();
|
||||
await locator.getByRole("button", { name: "Notification options" }).click();
|
||||
await page.getByRole("menuitemradio", { name: "Mute room" }).click();
|
||||
|
||||
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
||||
await app.client.createRoom({ name: "Dummy" });
|
||||
|
||||
@@ -175,7 +207,9 @@ test.describe("Sliding Sync", () => {
|
||||
// wait for this message to arrive, tell by the room list resorting
|
||||
await checkOrder(["Test Room", "Dummy"], page);
|
||||
|
||||
await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached();
|
||||
await expect(
|
||||
page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"),
|
||||
).not.toBeAttached();
|
||||
});
|
||||
|
||||
test("should update user settings promptly", async ({ page, app }) => {
|
||||
@@ -187,37 +221,6 @@ test.describe("Sliding Sync", () => {
|
||||
await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached();
|
||||
});
|
||||
|
||||
test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => {
|
||||
// create rooms and check room names are correct
|
||||
const roomIds: string[] = [];
|
||||
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
||||
const id = await app.client.createRoom({ name: fruit });
|
||||
roomIds.push(id);
|
||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||
}
|
||||
const [roomAId, roomPId] = roomIds;
|
||||
|
||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return body.room_subscriptions?.[subRoomId];
|
||||
};
|
||||
|
||||
// Select the Test Room and wait for playwright to get the request
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||
]);
|
||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||
|
||||
// Switch to another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("should show and be able to accept/reject/rescind invites", async ({
|
||||
page,
|
||||
app,
|
||||
@@ -358,4 +361,52 @@ test.describe("Sliding Sync", () => {
|
||||
// ensure the reply-to does not disappear
|
||||
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => {
|
||||
// create rooms and check room names are correct
|
||||
const roomIds: string[] = [];
|
||||
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
||||
const id = await app.client.createRoom({ name: fruit });
|
||||
roomIds.push(id);
|
||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||
}
|
||||
const [roomAId, roomPId, roomOId] = roomIds;
|
||||
|
||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return body.txn_id && body.room_subscriptions?.[subRoomId];
|
||||
};
|
||||
const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return (
|
||||
body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId]
|
||||
);
|
||||
};
|
||||
|
||||
// Select the Test Room and wait for playwright to get the request
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||
]);
|
||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||
|
||||
// Switch to another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// And switch to even another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomOId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomPId)),
|
||||
page.getByRole("treeitem", { name: "Orange", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// TODO: Add tests for encrypted rooms
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -35,18 +35,17 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
|
||||
name: spaceName,
|
||||
},
|
||||
},
|
||||
...roomIds.map((r) => spaceChildInitialState(r)),
|
||||
...roomIds.map(spaceChildInitialState),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
return {
|
||||
type: "m.space.child",
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: [roomId.split(":")[1]],
|
||||
order,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,10 +121,9 @@ test.describe("Spaces", () => {
|
||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||
|
||||
// Assert rooms exist in the room list
|
||||
const roomList = page.getByRole("tree", { name: "Rooms" });
|
||||
await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||
|
||||
// Assert rooms exist in the space explorer
|
||||
await expect(
|
||||
@@ -157,7 +155,7 @@ test.describe("Spaces", () => {
|
||||
|
||||
await page.getByRole("button", { name: "Just me" }).click();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||
await page.getByText("Sample Room").click({ force: true }); // force click as checkbox size is zero
|
||||
|
||||
// Temporal implementation as multiple elements with the role "button" and name "Add" are found
|
||||
await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click();
|
||||
@@ -167,50 +165,6 @@ test.describe("Spaces", () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should allow user to add an existing room to a space after creation",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await app.client.createRoom({
|
||||
name: "Sample Room",
|
||||
});
|
||||
await app.client.createRoom({
|
||||
name: "A Room that will not be selected",
|
||||
});
|
||||
|
||||
const menu = await openSpaceCreateMenu(page);
|
||||
await menu.getByRole("button", { name: "Private" }).click();
|
||||
|
||||
await menu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||
await menu
|
||||
.getByRole("textbox", { name: "Description" })
|
||||
.fill("This is a personal space to mourn Riot.im...");
|
||||
await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
|
||||
await menu.getByRole("textbox", { name: "Name" }).press("Enter");
|
||||
|
||||
await page.getByRole("button", { name: "Just me" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "Add existing room" }).click();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||
|
||||
await expect(page.getByRole("dialog", { name: "Avatar Add existing rooms" })).toMatchScreenshot(
|
||||
"add-existing-rooms-dialog.png",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Add" }).click();
|
||||
await expect(
|
||||
page.locator(".mx_SpaceHierarchy_list").getByRole("treeitem", { name: "Sample Room" }),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
|
||||
await app.client.createSpace({
|
||||
visibility: "public" as any,
|
||||
@@ -337,36 +291,4 @@ test.describe("Spaces", () => {
|
||||
// Assert we get shown the new room intro, and thus not the soft crash screen
|
||||
await expect(page.locator(".mx_NewRoomIntro")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render spaces view", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||
axe.disableRules([
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
"nested-interactive",
|
||||
// XXX: We have some known contrast issues here
|
||||
"color-contrast",
|
||||
]);
|
||||
|
||||
const childSpaceId1 = await app.client.createSpace({
|
||||
name: "Child Space 1",
|
||||
initial_state: [],
|
||||
});
|
||||
const childSpaceId2 = await app.client.createSpace({
|
||||
name: "Child Space 2",
|
||||
initial_state: [],
|
||||
});
|
||||
const childSpaceId3 = await app.client.createSpace({
|
||||
name: "Child Space 3",
|
||||
initial_state: [],
|
||||
});
|
||||
await app.client.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [
|
||||
spaceChildInitialState(childSpaceId1, "a"),
|
||||
spaceChildInitialState(childSpaceId2, "b"),
|
||||
spaceChildInitialState(childSpaceId3, "c"),
|
||||
],
|
||||
});
|
||||
await app.viewSpaceByName("Root Space");
|
||||
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,8 +28,6 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||
|
||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||
};
|
||||
@@ -918,27 +916,7 @@ test.describe("Timeline", () => {
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
});
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MVideoBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the video is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
|
||||
await expect(page.locator("video")).not.toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 46 KiB |
@@ -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:d19854a3dbbb4d5d24d84767d17e1a623181ae5f2bdda3505819c05a8d3c8611";
|
||||
const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
@import "./views/dialogs/_AddExistingToSpaceDialog.pcss";
|
||||
@import "./views/dialogs/_AnalyticsLearnMoreDialog.pcss";
|
||||
@import "./views/dialogs/_BugReportDialog.pcss";
|
||||
@import "./views/dialogs/_BulkRedactDialog.pcss";
|
||||
@import "./views/dialogs/_ChangelogDialog.pcss";
|
||||
@import "./views/dialogs/_CompoundDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
|
||||
@@ -152,7 +153,6 @@
|
||||
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
||||
@import "./views/dialogs/_PollCreateDialog.pcss";
|
||||
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
||||
@import "./views/dialogs/_ReportRoomDialog.pcss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
||||
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
||||
@@ -212,6 +212,7 @@
|
||||
@import "./views/elements/_ServerPicker.pcss";
|
||||
@import "./views/elements/_SettingsFlag.pcss";
|
||||
@import "./views/elements/_Spinner.pcss";
|
||||
@import "./views/elements/_StyledCheckbox.pcss";
|
||||
@import "./views/elements/_StyledRadioButton.pcss";
|
||||
@import "./views/elements/_SyntaxHighlight.pcss";
|
||||
@import "./views/elements/_TagComposer.pcss";
|
||||
@@ -227,7 +228,6 @@
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_EventTileBubble.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
|
||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||
@import "./views/messages/_MEmoteBody.pcss";
|
||||
@@ -270,7 +270,6 @@
|
||||
@import "./views/right_panel/_VerificationPanel.pcss";
|
||||
@import "./views/right_panel/_WidgetCard.pcss";
|
||||
@import "./views/room_settings/_AliasSettings.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -16,9 +16,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_SelectableDeviceTile_checkbox {
|
||||
flex: 1 0;
|
||||
|
||||
> div {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: var(--cpd-space-1x);
|
||||
.mx_Checkbox_background + div {
|
||||
flex: 1 0;
|
||||
/* override more specific selector */
|
||||
margin-left: $spacing-16 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -70,26 +70,38 @@ Please see LICENSE files in the repository root for full details.
|
||||
text-transform: uppercase;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
margin: 20px 0 12px;
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_pinToSidebarHeading {
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_favouritesCheckbox,
|
||||
.mx_QuickSettingsButton_peopleCheckbox {
|
||||
.mx_Checkbox_background + div {
|
||||
padding-left: 22px;
|
||||
position: relative;
|
||||
margin-left: 6px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_moreOptionsButton {
|
||||
margin-left: var(--cpd-space-7x);
|
||||
padding-left: 22px;
|
||||
margin-left: 22px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: var(--cpd-color-text-primary);
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_option {
|
||||
margin-bottom: var(--cpd-space-3x);
|
||||
label {
|
||||
/* Correctly line up icons and text. */
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_ContextMenuWrapper_new_room_list {
|
||||
@@ -99,10 +111,15 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_QuickSettingsButton_icon {
|
||||
margin-right: var(--cpd-space-1x);
|
||||
// TODO remove when all icons have fill=currentColor
|
||||
* {
|
||||
fill: $secondary-content;
|
||||
}
|
||||
color: $secondary-content;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -247,6 +247,15 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
padding: 3px 16px; /* to account for the 1px border */
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
display: inline-flex;
|
||||
|
||||
label {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -32,11 +32,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_AddExistingToSpace_section {
|
||||
margin-right: 12px;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
// provides space for scrollbar so that checkbox and scrollbar do not collide
|
||||
|
||||
&:not(:first-child) {
|
||||
@@ -219,12 +214,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
|
||||
form {
|
||||
/* Align checkboxes. */
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ {
|
||||
margin-right: 12px;
|
||||
}
|
||||
@@ -238,4 +227,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
19
res/css/views/dialogs/_BulkRedactDialog.pcss
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||
|
||||
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_BulkRedactDialog {
|
||||
.mx_Checkbox,
|
||||
.mx_BulkRedactDialog_checkboxMicrocopy {
|
||||
line-height: $font-20px;
|
||||
}
|
||||
|
||||
.mx_BulkRedactDialog_checkboxMicrocopy {
|
||||
margin-left: 26px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -43,6 +43,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_Field_valid.mx_Field:focus-within {
|
||||
border-color: $input-border-color;
|
||||
}
|
||||
|
||||
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
||||
background: $info-plinth-fg-color;
|
||||
border-color: $info-plinth-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ExportDialog_progress {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -74,6 +74,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
line-height: $font-15px;
|
||||
color: $tertiary-content;
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_ReportRoomDialog {
|
||||
textarea {
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
border-radius: 0.5rem;
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -19,6 +19,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin-top: 20px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-15px;
|
||||
|
||||
.mx_WidgetCapabilitiesPromptDialog_byline {
|
||||
color: $muted-fg-color;
|
||||
margin-left: 26px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Dialog_buttons {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -7,5 +7,26 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_LabelledCheckbox {
|
||||
margin-top: var(--cpd-space-2x);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
.mx_Checkbox {
|
||||
margin-top: 3px; /* visually align with label text */
|
||||
}
|
||||
|
||||
.mx_LabelledCheckbox_labels {
|
||||
flex: 1;
|
||||
|
||||
.mx_LabelledCheckbox_label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_LabelledCheckbox_byline {
|
||||
display: block;
|
||||
padding-top: $spacing-4;
|
||||
color: $muted-fg-color;
|
||||
font-size: $font-11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
res/css/views/elements/_StyledCheckbox.pcss
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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_Checkbox {
|
||||
$size: $font-16px;
|
||||
$border-radius: 0.27rem;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
& + label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& + label > .mx_Checkbox_background {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
height: $size;
|
||||
width: $size;
|
||||
size: 0.5rem;
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
box-sizing: border-box;
|
||||
border-radius: $border-radius;
|
||||
|
||||
.mx_Checkbox_checkmark {
|
||||
display: none;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
|
||||
mask-position: center;
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& + label > *:not(.mx_Checkbox_background) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:disabled + label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
& + label .mx_Checkbox_background {
|
||||
@mixin unreal-focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Checkbox.mx_Checkbox_kind_solid input[type="checkbox"] {
|
||||
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
||||
background: var(--cpd-color-icon-on-solid-primary);
|
||||
}
|
||||
|
||||
&:checked + label > .mx_Checkbox_background {
|
||||
background: var(--cpd-color-bg-accent-rest);
|
||||
border-color: var(--cpd-color-bg-accent-rest);
|
||||
}
|
||||
|
||||
&:checked:disabled + label > .mx_Checkbox_background {
|
||||
background: var(--cpd-color-bg-action-primary-disabled);
|
||||
border-color: var(--cpd-color-bg-action-primary-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Checkbox.mx_Checkbox_kind_outline input[type="checkbox"] {
|
||||
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
||||
background: var(--cpd-color-bg-accent-rest);
|
||||
}
|
||||
|
||||
&:checked + label > .mx_Checkbox_background {
|
||||
background: transparent;
|
||||
border-color: var(--cpd-color-bg-accent-rest);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.mx_HiddenMediaPlaceholder {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
> div {
|
||||
color: $accent;
|
||||
/* Icon alignment */
|
||||
display: flex;
|
||||
> svg {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
@@ -79,3 +79,39 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: $imagebody-giflabel-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_HiddenImagePlaceholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
.mx_HiddenImagePlaceholder_button {
|
||||
color: $accent;
|
||||
|
||||
span.mx_HiddenImagePlaceholder_eye {
|
||||
margin-right: 8px;
|
||||
|
||||
background-color: $accent;
|
||||
mask-image: url("$(res)/img/element-icons/eye.svg");
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
span:not(.mx_HiddenImagePlaceholder_eye) {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenImagePlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin: $spacing-12 0 $spacing-4;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_bottomOptions {
|
||||
.mx_RoomSummaryCard_leave {
|
||||
margin: 0 0 var(--cpd-space-8x);
|
||||
}
|
||||
|
||||
@@ -1,33 +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_EmptyRoomList_GenericPlaceholder {
|
||||
align-self: center;
|
||||
/** It should take 2/3 of the width **/
|
||||
width: 66%;
|
||||
/** It should be positioned at 1/3 of the height **/
|
||||
padding-top: 33%;
|
||||
|
||||
.mx_EmptyRoomList_GenericPlaceholder_title {
|
||||
font: var(--cpd-font-body-lg-semibold);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_EmptyRoomList_GenericPlaceholder_description {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_EmptyRoomList_DefaultPlaceholder {
|
||||
margin-top: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,9 @@
|
||||
|
||||
.mx_RoomList {
|
||||
height: 100%;
|
||||
|
||||
.mx_RoomList_List {
|
||||
/* Avoid when on hover, the background color to be on top of the right border */
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,3 @@
|
||||
.mx_RoomListItemView_menu_open {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_selected {
|
||||
background-color: var(--cpd-color-bg-action-secondary-pressed);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -393,7 +393,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton {
|
||||
.mx_StyledRadioButton,
|
||||
.mx_Checkbox {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -14,12 +14,17 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SidebarUserSettingsTab_icon {
|
||||
margin-right: var(--cpd-space-2x);
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
.mx_SidebarUserSettingsTab_checkbox {
|
||||
margin-bottom: $spacing-8;
|
||||
/* override checkbox styles */
|
||||
label {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.mx_SidebarUserSettingsTab_checkbox label {
|
||||
display: flex;
|
||||
svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: $spacing-8;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
||||
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
||||
import { graphemeSegmenter } from "./utils/strings";
|
||||
|
||||
export { Linkify, linkifyAndSanitizeHtml } from "./Linkify";
|
||||
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
|
||||
|
||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||
@@ -365,6 +365,53 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
|
||||
}
|
||||
}
|
||||
|
||||
export function bodyToDiv(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
ref?: React.Ref<HTMLDivElement>,
|
||||
): ReactNode {
|
||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
||||
|
||||
return formattedBody ? (
|
||||
<div
|
||||
key="body"
|
||||
ref={ref}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
||||
dir="auto"
|
||||
/>
|
||||
) : (
|
||||
<div key="body" ref={ref} className={className} dir="auto">
|
||||
{emojiBodyElements || strippedBody}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function bodyToSpan(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
ref?: React.Ref<HTMLSpanElement>,
|
||||
includeDir = true,
|
||||
): ReactNode {
|
||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
||||
|
||||
return formattedBody ? (
|
||||
<span
|
||||
key="body"
|
||||
ref={ref}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
||||
dir={includeDir ? "auto" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<span key="body" ref={ref} className={className} dir={includeDir ? "auto" : undefined}>
|
||||
{emojiBodyElements || strippedBody}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface BodyToNodeReturn {
|
||||
strippedBody: string;
|
||||
formattedBody?: string;
|
||||
@@ -372,11 +419,7 @@ interface BodyToNodeReturn {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export function bodyToNode(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
): BodyToNodeReturn {
|
||||
function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): BodyToNodeReturn {
|
||||
const eventInfo = analyseEvent(content, highlights, opts);
|
||||
|
||||
let emojiBody = false;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type MatrixCall,
|
||||
} from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
@@ -595,7 +596,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
|
||||
switch (newState) {
|
||||
case CallState.Ringing: {
|
||||
const incomingCallPushRule = MatrixClientPeg.safeGet().pushProcessor.getPushRuleById(
|
||||
const incomingCallPushRule = new PushProcessor(MatrixClientPeg.safeGet()).getPushRuleById(
|
||||
RuleId.IncomingCall,
|
||||
);
|
||||
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||
|
||||
@@ -149,7 +149,6 @@ interface ILoadSessionOpts {
|
||||
ignoreGuest?: boolean;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentQueryParams?: QueryDict;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,7 +196,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
|
||||
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
||||
logger.log("Using guest access credentials");
|
||||
await doSetLoggedIn(
|
||||
return doSetLoggedIn(
|
||||
{
|
||||
userId: fragmentQueryParams.guest_user_id as string,
|
||||
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||
@@ -207,8 +206,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
},
|
||||
true,
|
||||
false,
|
||||
);
|
||||
return true;
|
||||
).then(() => true);
|
||||
}
|
||||
const success = await restoreSessionFromStorage({
|
||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||
@@ -227,11 +225,6 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
// fall back to welcome screen
|
||||
return false;
|
||||
} catch (e) {
|
||||
// We may be aborted e.g. because our token expired, so don't show an error here
|
||||
if (opts.abortSignal?.aborted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e instanceof AbortLoginAndRebuildStorage) {
|
||||
// If we're aborting login because of a storage inconsistency, we don't
|
||||
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||
@@ -243,7 +236,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
return false;
|
||||
}
|
||||
|
||||
return handleLoadSessionFailure(e, opts);
|
||||
return handleLoadSessionFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,39 +406,6 @@ export function attemptTokenLogin(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the pickle key inside the credentials or create it if it does not exist for this device.
|
||||
*
|
||||
* @param credentials Holds the device to load/store the pickle key
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the loaded or generated pickle key or undefined if
|
||||
* none was loaded nor generated
|
||||
*/
|
||||
async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<string | undefined> {
|
||||
// Try to load the pickle key
|
||||
const userId = credentials.userId;
|
||||
const deviceId = credentials.deviceId;
|
||||
let pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined;
|
||||
if (!pickleKey) {
|
||||
// Create it if it did not exist
|
||||
pickleKey =
|
||||
userId && deviceId
|
||||
? ((await PlatformPeg.get()?.createPickleKey(userId, deviceId)) ?? undefined)
|
||||
: undefined;
|
||||
if (pickleKey) {
|
||||
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
||||
} else {
|
||||
logger.log("Pickle key not created");
|
||||
}
|
||||
} else {
|
||||
logger.log(
|
||||
`Pickle key already exists for ${credentials.userId}|${credentials.deviceId} do not create a new one`,
|
||||
);
|
||||
}
|
||||
|
||||
return pickleKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful token login or OIDC authorization.
|
||||
* Clear storage then save new credentials in storage
|
||||
@@ -453,8 +413,6 @@ async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<s
|
||||
*/
|
||||
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
||||
await clearStorage();
|
||||
// SSO does not go through setLoggedIn so we need to load/create the pickle key here too
|
||||
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||
await persistCredentials(credentials);
|
||||
|
||||
// remember that we just logged in
|
||||
@@ -663,7 +621,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise<boolean> {
|
||||
async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
||||
logger.error("Unable to load session", e);
|
||||
|
||||
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
||||
@@ -678,7 +636,7 @@ async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessi
|
||||
}
|
||||
|
||||
// try, try again
|
||||
return loadSession(loadSessionOpts);
|
||||
return loadSession();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,8 +655,18 @@ async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessi
|
||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
credentials.freshLogin = true;
|
||||
stopMatrixClient();
|
||||
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||
return doSetLoggedIn(credentials, true, true);
|
||||
const pickleKey =
|
||||
credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
if (pickleKey) {
|
||||
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
||||
} else {
|
||||
logger.log("Pickle key not created");
|
||||
}
|
||||
|
||||
return doSetLoggedIn({ ...credentials, pickleKey: pickleKey ?? undefined }, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1156,13 +1124,12 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
|
||||
baseUrl: hsUrl,
|
||||
accessToken,
|
||||
});
|
||||
const { user_id: userId, device_id: deviceId } = await tempClient.whoami();
|
||||
const { user_id: userId } = await tempClient.whoami();
|
||||
await doSetLoggedIn(
|
||||
{
|
||||
homeserverUrl: hsUrl,
|
||||
accessToken,
|
||||
userId,
|
||||
deviceId,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
|
||||
@@ -11,7 +11,12 @@ import sanitizeHtml, { type IOptions } from "sanitize-html";
|
||||
import { merge } from "lodash";
|
||||
import _Linkify from "linkify-react";
|
||||
|
||||
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
|
||||
import {
|
||||
_linkifyElement,
|
||||
_linkifyString,
|
||||
ELEMENT_URL_PATTERN,
|
||||
options as linkifyMatrixOptions,
|
||||
} from "./linkify-matrix";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
@@ -218,6 +223,17 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri
|
||||
return _linkifyString(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
|
||||
*
|
||||
* @param {object} element DOM element to linkify
|
||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions
|
||||
* @returns {object}
|
||||
*/
|
||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement {
|
||||
return _linkifyElement(element, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkify the given string and sanitize the HTML afterwards.
|
||||
*
|
||||
|
||||
@@ -299,12 +299,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
opts.threadSupport = true;
|
||||
|
||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||
throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported");
|
||||
}
|
||||
|
||||
// If the user has enabled the labs feature for sliding sync, set it up
|
||||
// otherwise check if the feature is supported
|
||||
if (SettingsStore.getValue("feature_simplified_sliding_sync")) {
|
||||
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
||||
} else {
|
||||
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
||||
|
||||
@@ -238,25 +238,25 @@ export function determineUnreadState(
|
||||
room?: Room,
|
||||
threadId?: string,
|
||||
includeThreads?: boolean,
|
||||
): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } {
|
||||
): { level: NotificationLevel; symbol: string | null; count: number } {
|
||||
if (!room) {
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
||||
}
|
||||
|
||||
if (getUnsentMessages(room, threadId).length > 0) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent };
|
||||
}
|
||||
|
||||
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
||||
}
|
||||
|
||||
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
||||
}
|
||||
|
||||
const redNotifs = getUnreadNotificationCount(
|
||||
@@ -269,12 +269,12 @@ export function determineUnreadState(
|
||||
|
||||
const trueCount = greyNotifs || redNotifs;
|
||||
if (redNotifs > 0) {
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false };
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
|
||||
}
|
||||
|
||||
const markedUnreadState = getMarkedUnreadState(room);
|
||||
if (greyNotifs > 0 || markedUnreadState) {
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false };
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
|
||||
}
|
||||
|
||||
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
||||
@@ -293,6 +293,5 @@ export function determineUnreadState(
|
||||
symbol: null,
|
||||
count: trueCount,
|
||||
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
||||
invited: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 React from "react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ContentHelpers,
|
||||
Direction,
|
||||
|
||||
@@ -36,51 +36,45 @@ Please see LICENSE files in the repository root for full details.
|
||||
* list ops)
|
||||
*/
|
||||
|
||||
import { type MatrixClient, ClientEvent, EventType, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
type MSC3575Filter,
|
||||
type MSC3575List,
|
||||
type MSC3575SlidingSyncResponse,
|
||||
MSC3575_STATE_KEY_LAZY,
|
||||
MSC3575_STATE_KEY_ME,
|
||||
MSC3575_WILDCARD,
|
||||
SlidingSync,
|
||||
SlidingSyncEvent,
|
||||
SlidingSyncState,
|
||||
} from "matrix-js-sdk/src/sliding-sync";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
||||
|
||||
// how long to long poll for
|
||||
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
||||
|
||||
// The state events we will get for every single room/space/old room/etc
|
||||
// This list is only augmented when a direct room subscription is made. (e.g you view a room)
|
||||
const REQUIRED_STATE_LIST = [
|
||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
||||
[EventType.RoomAvatar, ""], // any room avatar
|
||||
[EventType.RoomCanonicalAlias, ""], // for room name calculations
|
||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||
];
|
||||
|
||||
// the things to fetch when a user clicks on a room
|
||||
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
||||
timeline_limit: 50,
|
||||
// missing required_state which will change depending on the kind of room
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
required_state: [
|
||||
// state needed to handle space navigation and tombstone chains
|
||||
[EventType.RoomCreate, ""],
|
||||
[EventType.RoomTombstone, ""],
|
||||
[EventType.SpaceChild, MSC3575_WILDCARD],
|
||||
[EventType.SpaceParent, MSC3575_WILDCARD],
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME],
|
||||
],
|
||||
},
|
||||
};
|
||||
// lazy load room members so rooms like Matrix HQ don't take forever to load
|
||||
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
|
||||
const UNENCRYPTED_SUBSCRIPTION = {
|
||||
required_state: [
|
||||
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
|
||||
],
|
||||
@@ -96,72 +90,6 @@ const ENCRYPTED_SUBSCRIPTION = {
|
||||
...DEFAULT_ROOM_SUBSCRIPTION_INFO,
|
||||
};
|
||||
|
||||
// the complete set of lists made in SSS. The manager will spider all of these lists depending
|
||||
// on the count for each one.
|
||||
const sssLists: Record<string, MSC3575List> = {
|
||||
spaces: {
|
||||
ranges: [[0, 10]],
|
||||
timeline_limit: 0, // we don't care about the most recent message for spaces
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
},
|
||||
filters: {
|
||||
room_types: ["m.space"],
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
ranges: [[0, 10]],
|
||||
timeline_limit: 1, // most recent message display
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
},
|
||||
filters: {
|
||||
is_invite: true,
|
||||
},
|
||||
},
|
||||
favourites: {
|
||||
ranges: [[0, 10]],
|
||||
timeline_limit: 1, // most recent message display
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
},
|
||||
filters: {
|
||||
tags: ["m.favourite"],
|
||||
},
|
||||
},
|
||||
dms: {
|
||||
ranges: [[0, 10]],
|
||||
timeline_limit: 1, // most recent message display
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
},
|
||||
filters: {
|
||||
is_dm: true,
|
||||
is_invite: false,
|
||||
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
||||
not_tags: ["m.favourite", "m.lowpriority"],
|
||||
},
|
||||
},
|
||||
untagged: {
|
||||
// SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites"
|
||||
ranges: [[0, 10]],
|
||||
timeline_limit: 1, // most recent message display
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: REQUIRED_STATE_LIST,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type PartialSlidingSyncRequest = {
|
||||
filters?: MSC3575Filter;
|
||||
sort?: string[];
|
||||
@@ -175,8 +103,6 @@ export type PartialSlidingSyncRequest = {
|
||||
* sync options and code.
|
||||
*/
|
||||
export class SlidingSyncManager {
|
||||
public static serverSupportsSlidingSync: boolean;
|
||||
|
||||
public static readonly ListSpaces = "space_list";
|
||||
public static readonly ListSearch = "search_list";
|
||||
private static readonly internalInstance = new SlidingSyncManager();
|
||||
@@ -190,17 +116,48 @@ export class SlidingSyncManager {
|
||||
return SlidingSyncManager.internalInstance;
|
||||
}
|
||||
|
||||
private configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||
this.client = client;
|
||||
// create the set of lists we will use.
|
||||
const lists = new Map();
|
||||
for (const listName in sssLists) {
|
||||
lists.set(listName, sssLists[listName]);
|
||||
}
|
||||
// by default use the encrypted subscription as that gets everything, which is a safer
|
||||
// default than potentially missing member events.
|
||||
this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
|
||||
this.slidingSync = new SlidingSync(
|
||||
proxyUrl,
|
||||
new Map(),
|
||||
ENCRYPTED_SUBSCRIPTION,
|
||||
client,
|
||||
SLIDING_SYNC_TIMEOUT_MS,
|
||||
);
|
||||
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
||||
// set the space list
|
||||
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
|
||||
ranges: [[0, 20]],
|
||||
sort: ["by_name"],
|
||||
slow_get_all_rooms: true,
|
||||
timeline_limit: 0,
|
||||
required_state: [
|
||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
||||
[EventType.RoomAvatar, ""], // any room avatar
|
||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||
],
|
||||
include_old_rooms: {
|
||||
timeline_limit: 0,
|
||||
required_state: [
|
||||
[EventType.RoomCreate, ""],
|
||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||
],
|
||||
},
|
||||
filters: {
|
||||
room_types: ["m.space"],
|
||||
},
|
||||
});
|
||||
this.configureDefer.resolve();
|
||||
return this.slidingSync;
|
||||
}
|
||||
@@ -263,113 +220,99 @@ export class SlidingSyncManager {
|
||||
return this.slidingSync!.getListParams(listKey)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces that the user has chosen to view the given room and that room will now
|
||||
* be displayed, so it should have more state loaded.
|
||||
* @param roomId The room to set visible
|
||||
*/
|
||||
public async setRoomVisible(roomId: string): Promise<void> {
|
||||
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
|
||||
await this.configureDefer.promise;
|
||||
const subscriptions = this.slidingSync!.getRoomSubscriptions();
|
||||
if (subscriptions.has(roomId)) return;
|
||||
|
||||
subscriptions.add(roomId);
|
||||
|
||||
const room = this.client?.getRoom(roomId);
|
||||
// default to safety: request all state if we can't work it out. This can happen if you
|
||||
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
||||
// about the room.
|
||||
let shouldLazyLoad = false;
|
||||
if (room) {
|
||||
// do not lazy load encrypted rooms as we need the entire member list.
|
||||
shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
||||
if (visible) {
|
||||
subscriptions.add(roomId);
|
||||
} else {
|
||||
subscriptions.delete(roomId);
|
||||
}
|
||||
logger.log("SlidingSync setRoomVisible:", roomId, "shouldLazyLoad:", shouldLazyLoad);
|
||||
const room = this.client?.getRoom(roomId);
|
||||
let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
||||
if (!room) {
|
||||
// default to safety: request all state if we can't work it out. This can happen if you
|
||||
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
||||
// about the room.
|
||||
shouldLazyLoad = false;
|
||||
}
|
||||
logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad);
|
||||
if (shouldLazyLoad) {
|
||||
// lazy load this room
|
||||
this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME);
|
||||
}
|
||||
this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
||||
const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
||||
if (room) {
|
||||
return; // we have data already for this room, show immediately e.g it's in a list
|
||||
return roomId; // we have data already for this room, show immediately e.g it's in a list
|
||||
}
|
||||
// wait until we know about this room. This may take a little while.
|
||||
return new Promise((resolve) => {
|
||||
logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`);
|
||||
const waitForRoom = (r: Room): void => {
|
||||
if (r.roomId === roomId) {
|
||||
this.client?.off(ClientEvent.Room, waitForRoom);
|
||||
logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.client?.on(ClientEvent.Room, waitForRoom);
|
||||
});
|
||||
try {
|
||||
// wait until the next sync before returning as RoomView may need to know the current state
|
||||
await p;
|
||||
} catch {
|
||||
logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction");
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all rooms on the user's account. Retrieval is gradual over time.
|
||||
* This function MUST be called BEFORE the first sync request goes out.
|
||||
* Retrieve all rooms on the user's account. Used for pre-populating the local search cache.
|
||||
* Retrieval is gradual over time.
|
||||
* @param batchSize The number of rooms to return in each request.
|
||||
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
||||
*/
|
||||
private async startSpidering(
|
||||
slidingSync: SlidingSync,
|
||||
batchSize: number,
|
||||
gapBetweenRequestsMs: number,
|
||||
): Promise<void> {
|
||||
// The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously.
|
||||
// There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than
|
||||
// potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this
|
||||
// way, the lists are effectively priority requests. We don't actually care which room goes into which list at this
|
||||
// point, as the RoomListStore will calculate this based on the returned data.
|
||||
|
||||
// copy the initial set of list names and ranges, we'll keep this map updated.
|
||||
const listToUpperBound = new Map(
|
||||
Object.keys(sssLists).map((listName) => {
|
||||
return [listName, sssLists[listName].ranges[0][1]];
|
||||
}),
|
||||
);
|
||||
console.log("startSpidering:", listToUpperBound);
|
||||
|
||||
// listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing
|
||||
// the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/
|
||||
// but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes
|
||||
// from request N.
|
||||
const lifecycle = async (
|
||||
state: SlidingSyncState,
|
||||
_: MSC3575SlidingSyncResponse | null,
|
||||
err?: Error,
|
||||
): Promise<void> => {
|
||||
if (state !== SlidingSyncState.Complete) {
|
||||
return;
|
||||
}
|
||||
await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
// for all lists with total counts > range => increase the range
|
||||
let hasSetRanges = false;
|
||||
listToUpperBound.forEach((currentUpperBound, listName) => {
|
||||
const totalCount = slidingSync.getListData(listName)?.joinedCount || 0;
|
||||
if (currentUpperBound < totalCount) {
|
||||
// increment the upper bound
|
||||
const newUpperBound = currentUpperBound + batchSize;
|
||||
console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`);
|
||||
listToUpperBound.set(listName, newUpperBound);
|
||||
// make the next request. This will only send the request when this callback has finished, so if
|
||||
// we set all the list ranges at once we will only send 1 new request.
|
||||
slidingSync.setListRanges(listName, [[0, newUpperBound]]);
|
||||
hasSetRanges = true;
|
||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
|
||||
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
|
||||
let startIndex = batchSize;
|
||||
let hasMore = true;
|
||||
let firstTime = true;
|
||||
while (hasMore) {
|
||||
const endIndex = startIndex + batchSize - 1;
|
||||
try {
|
||||
const ranges = [
|
||||
[0, batchSize - 1],
|
||||
[startIndex, endIndex],
|
||||
];
|
||||
if (firstTime) {
|
||||
await this.slidingSync!.setList(SlidingSyncManager.ListSearch, {
|
||||
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
|
||||
// any changes to the list whilst spidering are caught.
|
||||
ranges: ranges,
|
||||
sort: [
|
||||
"by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough
|
||||
],
|
||||
timeline_limit: 0, // we only care about the room details, not messages in the room
|
||||
required_state: [
|
||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
||||
[EventType.RoomAvatar, ""], // any room avatar
|
||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||
],
|
||||
// we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms
|
||||
// on the user's account. This means some data in the search dialog results may be inaccurate
|
||||
// e.g membership of space, but this will be corrected when the user clicks on the room
|
||||
// as the direct room subscription does include old room iterations.
|
||||
filters: {
|
||||
// we get spaces via a different list, so filter them out
|
||||
not_room_types: ["m.space"],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges);
|
||||
}
|
||||
});
|
||||
if (!hasSetRanges) {
|
||||
// finish spidering
|
||||
slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||
} catch {
|
||||
// do nothing, as we reject only when we get interrupted but that's fine as the next
|
||||
// request will include our data
|
||||
} finally {
|
||||
// gradually request more over time, even on errors.
|
||||
await sleep(gapBetweenRequestsMs);
|
||||
}
|
||||
};
|
||||
slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||
const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!;
|
||||
hasMore = endIndex + 1 < listData.joinedCount;
|
||||
startIndex += batchSize;
|
||||
firstTime = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,10 +325,42 @@ export class SlidingSyncManager {
|
||||
* @returns A working Sliding Sync or undefined
|
||||
*/
|
||||
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
||||
const slidingSync = this.configure(client, client.baseUrl);
|
||||
logger.info("Simplified Sliding Sync activated at", client.baseUrl);
|
||||
this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart
|
||||
return slidingSync;
|
||||
const baseUrl = client.baseUrl;
|
||||
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||
const wellKnownProxyUrl = await this.getProxyFromWellKnown(client);
|
||||
|
||||
const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl;
|
||||
|
||||
this.configure(client, slidingSyncEndpoint);
|
||||
logger.info("Sliding sync activated at", slidingSyncEndpoint);
|
||||
this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart
|
||||
|
||||
return this.slidingSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sliding sync proxy URL from the client well known
|
||||
* @param client The MatrixClient to use
|
||||
* @return The proxy url
|
||||
*/
|
||||
public async getProxyFromWellKnown(client: MatrixClient): Promise<string | undefined> {
|
||||
let proxyUrl: string | undefined;
|
||||
|
||||
try {
|
||||
const clientDomain = await client.getDomain();
|
||||
if (clientDomain === null) {
|
||||
throw new RangeError("Homeserver domain is null");
|
||||
}
|
||||
const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain);
|
||||
proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url;
|
||||
} catch {
|
||||
// Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown
|
||||
}
|
||||
|
||||
if (proxyUrl != undefined) {
|
||||
logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl);
|
||||
}
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,9 +371,9 @@ export class SlidingSyncManager {
|
||||
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
||||
// Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561
|
||||
// `client` can be undefined/null in tests for some reason.
|
||||
const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575");
|
||||
const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575");
|
||||
if (support) {
|
||||
logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable");
|
||||
logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable");
|
||||
}
|
||||
return support;
|
||||
}
|
||||
@@ -412,9 +387,20 @@ export class SlidingSyncManager {
|
||||
*/
|
||||
public async checkSupport(client: MatrixClient): Promise<void> {
|
||||
if (await this.nativeSlidingSyncSupport(client)) {
|
||||
SlidingSyncManager.serverSupportsSlidingSync = true;
|
||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
||||
return;
|
||||
}
|
||||
SlidingSyncManager.serverSupportsSlidingSync = false;
|
||||
|
||||
const proxyUrl = await this.getProxyFromWellKnown(client);
|
||||
if (proxyUrl != undefined) {
|
||||
const response = await fetch(new URL("/client/server.json", proxyUrl), {
|
||||
method: Method.Get,
|
||||
signal: timeoutSignal(10 * 1000), // 10s
|
||||
});
|
||||
if (response.status === 200) {
|
||||
logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl);
|
||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import shouldHideEvent from "./shouldHideEvent";
|
||||
import { haveRendererForEvent } from "./events/EventTileFactory";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs";
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,12 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
|
||||
}
|
||||
|
||||
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
|
||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||
// TODO: https://github.com/vector-im/element-web/issues/23207
|
||||
// Sliding Sync doesn't support unread indicator dots (yet...)
|
||||
return false;
|
||||
}
|
||||
|
||||
const toCheck: Array<Room | Thread> = [room];
|
||||
if (includeThreads) {
|
||||
toCheck.push(...room.getThreads());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
@@ -16,12 +16,13 @@ import { KeyBindingAction } from "../KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
||||
label?: string;
|
||||
onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent
|
||||
onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton
|
||||
}
|
||||
|
||||
// Semantic component for representing a styled role=menuitemcheckbox
|
||||
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, onChange, onClose, ...props }) => {
|
||||
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent): void => {
|
||||
@@ -62,6 +63,7 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, onChange, o
|
||||
<StyledCheckbox
|
||||
{...props}
|
||||
role="menuitemcheckbox"
|
||||
aria-label={label}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
|
||||
@@ -40,8 +40,8 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private dispatcherRef?: string;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
page: "",
|
||||
|
||||
@@ -235,7 +235,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private themeWatcher?: ThemeWatcher;
|
||||
private fontWatcher?: FontWatcher;
|
||||
private readonly stores: SdkContextClass;
|
||||
private loadSessionAbortController = new AbortController();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -328,7 +327,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// When the session loads it'll be detected as soft logged out and a dispatch
|
||||
// will be sent out to say that, triggering this MatrixChat to show the soft
|
||||
// logout page.
|
||||
Lifecycle.loadSession({ abortSignal: this.loadSessionAbortController.signal });
|
||||
Lifecycle.loadSession();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -553,7 +552,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
||||
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
abortSignal: this.loadSessionAbortController.signal,
|
||||
});
|
||||
})
|
||||
.then((loadedSession) => {
|
||||
@@ -1390,7 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// so show the homepage.
|
||||
dis.dispatch<ViewHomePagePayload>({ action: Action.ViewHomePage, justRegistered: true });
|
||||
}
|
||||
} else if (!(await this.shouldForceVerification())) {
|
||||
} else {
|
||||
this.showScreenAfterLogin();
|
||||
}
|
||||
|
||||
@@ -1567,33 +1565,26 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
});
|
||||
|
||||
cli.on(HttpApiEvent.SessionLoggedOut, (errObj) => {
|
||||
this.loadSessionAbortController.abort(errObj);
|
||||
this.loadSessionAbortController = new AbortController();
|
||||
|
||||
cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
// A modal might have been open when we were logged out by the server
|
||||
Modal.forceCloseAllModals();
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data?.["soft_logout"]) {
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
|
||||
logger.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
return;
|
||||
}
|
||||
|
||||
dis.dispatch(
|
||||
{
|
||||
action: "logout",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// The above dispatch closes all modals, so open the modal after calling it synchronously
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("auth|session_logged_out_title"),
|
||||
description: _t("auth|session_logged_out_description"),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: "logout",
|
||||
});
|
||||
});
|
||||
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
|
||||
Modal.createDialog(
|
||||
@@ -2012,17 +2003,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
private onCompleteSecurityE2eSetupFinished = async (): Promise<void> => {
|
||||
const forceVerify = await this.shouldForceVerification();
|
||||
if (forceVerify) {
|
||||
const isVerified = await MatrixClientPeg.safeGet().getCrypto()?.isCrossSigningReady();
|
||||
if (!isVerified) {
|
||||
// We must verify but we haven't yet verified - don't continue logging in
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.onShowPostLoginScreen().catch((e) => {
|
||||
private onCompleteSecurityE2eSetupFinished = (): void => {
|
||||
// This is async but we making this function async to wait for it isn't useful
|
||||
this.onShowPostLoginScreen().catch((e) => {
|
||||
logger.error("Exception showing post-login screen", e);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -259,8 +259,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
|
||||
public grouperKeyMap = new WeakMap<MatrixEvent, string>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// previous positions the read marker has been in, so we can
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 React from "react";
|
||||
import * as React from "react";
|
||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { type ComponentClass } from "../../@types/common";
|
||||
|
||||
@@ -38,8 +38,8 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
||||
|
||||
private card = React.createRef<HTMLDivElement>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
narrow: false,
|
||||
|
||||
@@ -64,10 +64,8 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = RightPanel.getDerivedStateFromProps(props);
|
||||
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
private readonly delayedUpdate = throttle(
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { ALTERNATE_KEY_NAME } from "../../accessibility/KeyboardShortcuts";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021-2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -16,7 +16,6 @@ import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -117,7 +116,6 @@ const Tile: React.FC<ITileProps> = ({
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const checkboxLabelId = useId();
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
@@ -174,14 +172,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
let checkbox: ReactElement | undefined;
|
||||
if (onToggleClick) {
|
||||
if (hasPermissions) {
|
||||
checkbox = (
|
||||
<StyledCheckbox
|
||||
role="presentation"
|
||||
aria-labelledby={checkboxLabelId}
|
||||
checked={!!selected}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
);
|
||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
|
||||
} else {
|
||||
checkbox = (
|
||||
<TextWithTooltip
|
||||
@@ -190,12 +181,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<StyledCheckbox
|
||||
role="presentation"
|
||||
aria-labelledby={checkboxLabelId}
|
||||
disabled={true}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
|
||||
</TextWithTooltip>
|
||||
);
|
||||
}
|
||||
@@ -262,7 +248,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
<div className="mx_SpaceHierarchy_roomTile_item">
|
||||
<div className="mx_SpaceHierarchy_roomTile_avatar">{avatar}</div>
|
||||
<div className="mx_SpaceHierarchy_roomTile_name">
|
||||
<span id={checkboxLabelId}>{name}</span>
|
||||
{name}
|
||||
{joinedSection}
|
||||
{suggestedSection}
|
||||
</div>
|
||||
@@ -344,14 +330,11 @@ const Tile: React.FC<ITileProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
const shouldToggle = hasPermissions && onToggleClick;
|
||||
|
||||
return (
|
||||
<li
|
||||
className="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
aria-selected={selected}
|
||||
aria-labelledby={checkboxLabelId}
|
||||
aria-expanded={children ? showChildren : undefined}
|
||||
>
|
||||
<AccessibleButton
|
||||
@@ -359,7 +342,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
mx_SpaceHierarchy_subspace: room.room_type === RoomType.Space,
|
||||
mx_SpaceHierarchy_joining: busy,
|
||||
})}
|
||||
onClick={shouldToggle ? onToggleClick : onPreviewClick}
|
||||
onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick}
|
||||
onKeyDown={onKeyDown}
|
||||
ref={ref}
|
||||
onFocus={onFocus}
|
||||
|
||||
@@ -86,8 +86,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
// Set by setEventId in ctor.
|
||||
private eventId!: string;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.setEventId(this.props.mxEvent);
|
||||
const thread = this.props.room.getThread(this.eventId) ?? undefined;
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 React from "react";
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Text } from "@vector-im/compound-web";
|
||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
@@ -83,8 +83,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private readonly dndWatcherRef?: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
@@ -370,13 +370,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
? toRightOf(this.state.contextMenuPosition)
|
||||
: below(this.state.contextMenuPosition);
|
||||
|
||||
const userIdentifierString = UserIdentifierCustomisations.getDisplayUserIdentifier(
|
||||
MatrixClientPeg.safeGet().getSafeUserId(),
|
||||
{
|
||||
withDisplayName: true,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<IconizedContextMenu {...position} onFinished={this.onCloseMenu} className="mx_UserMenu_contextMenu">
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
@@ -384,8 +377,13 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId" title={userIdentifierString || ""}>
|
||||
{userIdentifierString}
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{UserIdentifierCustomisations.getDisplayUserIdentifier(
|
||||
MatrixClientPeg.safeGet().getSafeUserId(),
|
||||
{
|
||||
withDisplayName: true,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export default class UserView extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -66,8 +66,8 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||
public static contextType = SDKContext;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
loginView: LoginView.Loading,
|
||||
|
||||
@@ -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 { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface MessagePreviewViewState {
|
||||
/**
|
||||
* A string representation of the message preview if available.
|
||||
*/
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for rendering a message preview for a given room list item.
|
||||
* @param room The room for which we're rendering the message preview.
|
||||
* @see {@link MessagePreviewViewState} for what this view model returns.
|
||||
*/
|
||||
export function useMessagePreviewViewModel(room: Room): MessagePreviewViewState {
|
||||
const [messagePreview, setMessagePreview] = useState<MessagePreview | null>(null);
|
||||
|
||||
const updatePreview = useCallback(async (): Promise<void> => {
|
||||
/**
|
||||
* The second argument to getPreviewForRoom is a tag id which doesn't really make
|
||||
* much sense within the context of the new room list. We can pass an empty string
|
||||
* to match all tags for now but we should remember to actually change the implementation
|
||||
* in the store once we remove the legacy room list.
|
||||
*/
|
||||
const newPreview = await MessagePreviewStore.instance.getPreviewForRoom(room, "");
|
||||
setMessagePreview(newPreview);
|
||||
}, [room]);
|
||||
|
||||
/**
|
||||
* Update when the message preview has changed for this room.
|
||||
*/
|
||||
useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
/**
|
||||
* Do an initial fetch of the message preview.
|
||||
*/
|
||||
useEffect(() => {
|
||||
updatePreview();
|
||||
}, [updatePreview]);
|
||||
|
||||
return {
|
||||
message: messagePreview?.text,
|
||||
};
|
||||
}
|
||||
@@ -6,8 +6,10 @@
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import { EventTimeline, EventType, JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
@@ -30,7 +32,6 @@ import {
|
||||
} from "../../../utils/space";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { createRoom, hasCreateRoomRights } from "./utils";
|
||||
|
||||
/**
|
||||
* Hook to get the active space and its title.
|
||||
@@ -127,7 +128,14 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
|
||||
const { activeSpace, title } = useSpace();
|
||||
const isSpaceRoom = Boolean(activeSpace);
|
||||
|
||||
const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace);
|
||||
const canCreateRoomInSpace = Boolean(
|
||||
activeSpace
|
||||
?.getLiveTimeline()
|
||||
.getState(EventTimeline.FORWARDS)
|
||||
?.maySendStateEvent(EventType.RoomAvatar, matrixClient.getSafeUserId()),
|
||||
);
|
||||
// If we are in a space, we check canCreateRoomInSpace
|
||||
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms) && (!isSpaceRoom || canCreateRoomInSpace);
|
||||
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms");
|
||||
const displayComposeMenu = canCreateRoom || canCreateVideoRoom;
|
||||
const displaySpaceMenu = isSpaceRoom;
|
||||
@@ -143,9 +151,13 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
|
||||
}, []);
|
||||
|
||||
const createRoomMemoized = useCallback(
|
||||
const createRoom = useCallback(
|
||||
(e: Event) => {
|
||||
createRoom(activeSpace);
|
||||
if (activeSpace) {
|
||||
showCreateNewRoom(activeSpace);
|
||||
} else {
|
||||
defaultDispatcher.fire(Action.CreateRoom);
|
||||
}
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
},
|
||||
[activeSpace],
|
||||
@@ -201,7 +213,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
|
||||
canInviteInSpace,
|
||||
canAccessSpaceSettings,
|
||||
createChatRoom,
|
||||
createRoom: createRoomMemoized,
|
||||
createRoom,
|
||||
createVideoRoom,
|
||||
openSpaceHome,
|
||||
inviteInSpace,
|
||||
|
||||
@@ -5,56 +5,21 @@ 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 } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms";
|
||||
import { type SortOption, useSorter } from "./useSorter";
|
||||
import { useMessagePreviewToggle } from "./useMessagePreviewToggle";
|
||||
import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useStickyRoomList } from "./useStickyRoomList";
|
||||
|
||||
export interface RoomListViewState {
|
||||
/**
|
||||
* A list of rooms to be displayed in the left panel.
|
||||
*/
|
||||
rooms: Room[];
|
||||
|
||||
/**
|
||||
* Create a chat room
|
||||
* @param e - The click event
|
||||
*/
|
||||
createChatRoom: () => void;
|
||||
|
||||
/**
|
||||
* Whether the user can create a room in the current space
|
||||
*/
|
||||
canCreateRoom: boolean;
|
||||
|
||||
/**
|
||||
* Create a room
|
||||
* @param e - The click event
|
||||
*/
|
||||
createRoom: () => void;
|
||||
|
||||
/**
|
||||
* A list of objects that provide the view enough information
|
||||
* to render primary room filters.
|
||||
*/
|
||||
primaryFilters: PrimaryFilter[];
|
||||
|
||||
/**
|
||||
* The currently active primary filter.
|
||||
* If no primary filter is active, this will be undefined.
|
||||
*/
|
||||
activePrimaryFilter?: PrimaryFilter;
|
||||
|
||||
/**
|
||||
* A function to activate a given secondary filter.
|
||||
*/
|
||||
@@ -74,21 +39,6 @@ export interface RoomListViewState {
|
||||
* The currently active sort option.
|
||||
*/
|
||||
activeSortOption: SortOption;
|
||||
|
||||
/**
|
||||
* Whether message previews must be shown or not.
|
||||
*/
|
||||
shouldShowMessagePreview: boolean;
|
||||
|
||||
/**
|
||||
* A function to turn on/off message previews.
|
||||
*/
|
||||
toggleMessagePreview: () => void;
|
||||
|
||||
/**
|
||||
* The index of the active room in the room list.
|
||||
*/
|
||||
activeIndex: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,42 +46,15 @@ export interface RoomListViewState {
|
||||
* @see {@link RoomListViewState} for more information about what this view model returns.
|
||||
*/
|
||||
export function useRoomListViewModel(): RoomListViewState {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const {
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
rooms: filteredRooms,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
} = useFilteredRooms();
|
||||
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
|
||||
|
||||
const currentSpace = useEventEmitterState<Room | null>(
|
||||
SpaceStore.instance,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
() => SpaceStore.instance.activeSpaceRoom,
|
||||
);
|
||||
const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace);
|
||||
|
||||
const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms();
|
||||
const { activeSortOption, sort } = useSorter();
|
||||
const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle();
|
||||
|
||||
const createChatRoom = useCallback(() => dispatcher.fire(Action.CreateChat), []);
|
||||
const createRoom = useCallback(() => createRoomFunc(currentSpace), [currentSpace]);
|
||||
|
||||
return {
|
||||
rooms,
|
||||
canCreateRoom,
|
||||
createRoom,
|
||||
createChatRoom,
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
activeSortOption,
|
||||
sort,
|
||||
shouldShowMessagePreview,
|
||||
toggleMessagePreview,
|
||||
activeIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ export interface PrimaryFilter {
|
||||
active: boolean;
|
||||
// Text that can be used in the UI to represent this filter.
|
||||
name: string;
|
||||
// The key of the filter
|
||||
key: FilterKey;
|
||||
}
|
||||
|
||||
interface FilteredRooms {
|
||||
@@ -36,11 +34,6 @@ interface FilteredRooms {
|
||||
rooms: Room[];
|
||||
activateSecondaryFilter: (filter: SecondaryFilters) => void;
|
||||
activeSecondaryFilter: SecondaryFilters;
|
||||
/**
|
||||
* The currently active primary filter.
|
||||
* If no primary filter is active, this will be undefined.
|
||||
*/
|
||||
activePrimaryFilter?: PrimaryFilter;
|
||||
}
|
||||
|
||||
const filterKeyToNameMap: Map<FilterKey, TranslationKey> = new Map([
|
||||
@@ -145,14 +138,22 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
// SecondaryFilter is an enum for the UI, let's convert it to something
|
||||
// that the store will understand.
|
||||
const secondary = secondaryFiltersToFilterKeyMap.get(filter);
|
||||
|
||||
// Active primary filter may need to be toggled off when applying this secondary filer.
|
||||
let primary = primaryFilter;
|
||||
if (
|
||||
primaryFilter !== undefined &&
|
||||
secondary !== undefined &&
|
||||
!isPrimaryFilterCompatible(primaryFilter, secondary)
|
||||
) {
|
||||
primary = undefined;
|
||||
}
|
||||
|
||||
setActiveSecondaryFilter(filter);
|
||||
|
||||
// Reset any active primary filters.
|
||||
setPrimaryFilter(undefined);
|
||||
|
||||
updateRoomsFromStore(filterUndefined([secondary]));
|
||||
setPrimaryFilter(primary);
|
||||
updateRoomsFromStore(filterUndefined([primary, secondary]));
|
||||
},
|
||||
[activeSecondaryFilter, updateRoomsFromStore],
|
||||
[activeSecondaryFilter, primaryFilter, updateRoomsFromStore],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -171,7 +172,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
},
|
||||
active: primaryFilter === key,
|
||||
name,
|
||||
key,
|
||||
};
|
||||
};
|
||||
const filters: PrimaryFilter[] = [];
|
||||
@@ -184,7 +184,5 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
return filters;
|
||||
}, [primaryFilter, updateRoomsFromStore, secondaryFilter]);
|
||||
|
||||
const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]);
|
||||
|
||||
return { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter };
|
||||
return { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter };
|
||||
}
|
||||
|
||||
@@ -1,36 +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 { useCallback, useState } from "react";
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface MessagePreviewToggleState {
|
||||
shouldShowMessagePreview: boolean;
|
||||
toggleMessagePreview: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook:
|
||||
* - Provides a state that tracks whether message previews are turned on or off.
|
||||
* - Provides a function to toggle message previews.
|
||||
*/
|
||||
export function useMessagePreviewToggle(): MessagePreviewToggleState {
|
||||
const [shouldShowMessagePreview, setShouldShowMessagePreview] = useState(() =>
|
||||
SettingsStore.getValue("RoomList.showMessagePreview"),
|
||||
);
|
||||
|
||||
const toggleMessagePreview = useCallback((): void => {
|
||||
setShouldShowMessagePreview((current) => {
|
||||
const toggled = !current;
|
||||
SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled);
|
||||
return toggled;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { toggleMessagePreview, shouldShowMessagePreview };
|
||||
}
|
||||
@@ -1,117 +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 { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Optional } from "matrix-events-sdk";
|
||||
|
||||
function getIndexByRoomId(rooms: Room[], roomId: Optional<string>): number | undefined {
|
||||
const index = rooms.findIndex((room) => room.roomId === roomId);
|
||||
return index === -1 ? undefined : index;
|
||||
}
|
||||
|
||||
function getRoomsWithStickyRoom(
|
||||
rooms: Room[],
|
||||
oldIndex: number | undefined,
|
||||
newIndex: number | undefined,
|
||||
isRoomChange: boolean,
|
||||
): { newRooms: Room[]; newIndex: number | undefined } {
|
||||
const updated = { newIndex, newRooms: rooms };
|
||||
if (isRoomChange) {
|
||||
/*
|
||||
* When opening another room, the index should obviously change.
|
||||
*/
|
||||
return updated;
|
||||
}
|
||||
if (newIndex === undefined || oldIndex === undefined) {
|
||||
/*
|
||||
* If oldIndex is undefined, then there was no active room before.
|
||||
* So nothing to do in regards to sticky room.
|
||||
* Similarly, if newIndex is undefined, there's no active room anymore.
|
||||
*/
|
||||
return updated;
|
||||
}
|
||||
if (newIndex === oldIndex) {
|
||||
/*
|
||||
* If the index hasn't changed, we have nothing to do.
|
||||
*/
|
||||
return updated;
|
||||
}
|
||||
if (oldIndex > rooms.length - 1) {
|
||||
/*
|
||||
* If the old index falls out of the bounds of the rooms array
|
||||
* (usually because rooms were removed), we can no longer place
|
||||
* the active room in the same old index.
|
||||
*/
|
||||
return updated;
|
||||
}
|
||||
|
||||
/*
|
||||
* Making the active room sticky is as simple as removing it from
|
||||
* its new index and placing it in the old index.
|
||||
*/
|
||||
const newRooms = [...rooms];
|
||||
const [newRoom] = newRooms.splice(newIndex, 1);
|
||||
newRooms.splice(oldIndex, 0, newRoom);
|
||||
|
||||
return { newIndex: oldIndex, newRooms };
|
||||
}
|
||||
|
||||
interface StickyRoomListResult {
|
||||
/**
|
||||
* List of rooms with sticky active room.
|
||||
*/
|
||||
rooms: Room[];
|
||||
/**
|
||||
* Index of the active room in the room list.
|
||||
*/
|
||||
activeIndex: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* - Provides a list of rooms such that the active room is sticky i.e the active room is kept
|
||||
* in the same index even when the order of rooms in the list changes.
|
||||
* - Provides the index of the active room.
|
||||
* @param rooms list of rooms
|
||||
* @see {@link StickyRoomListResult} details what this hook returns..
|
||||
*/
|
||||
export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
|
||||
const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({
|
||||
index: undefined,
|
||||
roomsWithStickyRoom: rooms,
|
||||
});
|
||||
|
||||
const updateRoomsAndIndex = useCallback(
|
||||
(newRoomId?: string, isRoomChange: boolean = false) => {
|
||||
setListState((current) => {
|
||||
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const newActiveIndex = getIndexByRoomId(rooms, activeRoomId);
|
||||
const oldIndex = current.index;
|
||||
const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange);
|
||||
return { index: newIndex, roomsWithStickyRoom: newRooms };
|
||||
});
|
||||
},
|
||||
[rooms],
|
||||
);
|
||||
|
||||
// Re-calculate the index when the active room has changed.
|
||||
useDispatcher(dispatcher, (payload) => {
|
||||
if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true);
|
||||
});
|
||||
|
||||
// Re-calculate the index when the list of rooms has changed.
|
||||
useEffect(() => {
|
||||
updateRoomsAndIndex();
|
||||
}, [rooms, updateRoomsAndIndex]);
|
||||
|
||||
return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom };
|
||||
}
|
||||
@@ -5,14 +5,11 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Room, KnownMembership, EventTimeline, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type Room, KnownMembership } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { isKnockDenied } from "../../../utils/membership";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { showCreateNewRoom } from "../../../utils/space";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
/**
|
||||
* Check if the user has access to the options menu.
|
||||
@@ -26,33 +23,3 @@ export function hasAccessToOptionsMenu(room: Room): boolean {
|
||||
shouldShowComponent(UIComponent.RoomOptionsMenu))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room
|
||||
* @param space - The space to create the room in
|
||||
*/
|
||||
export async function createRoom(space?: Room | null): Promise<void> {
|
||||
if (space) {
|
||||
await showCreateNewRoom(space);
|
||||
} else {
|
||||
dispatcher.fire(Action.CreateRoom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has the rights to create a room in the given space
|
||||
* If the space is not provided, it will check if the user has the rights to create a room in general
|
||||
* @param matrixClient
|
||||
* @param space
|
||||
*/
|
||||
export function hasCreateRoomRights(matrixClient: MatrixClient, space?: Room | null): boolean {
|
||||
const hasUIRight = shouldShowComponent(UIComponent.CreateRooms);
|
||||
if (!space || !hasUIRight) return hasUIRight;
|
||||
|
||||
return Boolean(
|
||||
space
|
||||
?.getLiveTimeline()
|
||||
.getState(EventTimeline.FORWARDS)
|
||||
?.maySendStateEvent(EventType.RoomAvatar, matrixClient.getSafeUserId()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ 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 React, { createRef } from "react";
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
||||
@@ -130,8 +130,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||
|
||||
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
canRedact: false,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { type ReactElement, type ReactNode, useContext, useId, useMemo, useRef, useState } from "react";
|
||||
import React, { type ReactElement, type ReactNode, useContext, useMemo, useRef, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { type Room, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
@@ -53,9 +53,8 @@ export const Entry: React.FC<{
|
||||
checked: boolean;
|
||||
onChange?(value: boolean): void;
|
||||
}> = ({ room, checked, onChange }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<li id={id} className="mx_AddExistingToSpace_entry" aria-label={room.name}>
|
||||
<label className="mx_AddExistingToSpace_entry">
|
||||
{room?.isSpaceRoom() ? (
|
||||
<RoomAvatar room={room} size="32px" />
|
||||
) : (
|
||||
@@ -63,12 +62,11 @@ export const Entry: React.FC<{
|
||||
)}
|
||||
<span className="mx_AddExistingToSpace_entry_name">{room.name}</span>
|
||||
<StyledCheckbox
|
||||
aria-labelledby={id}
|
||||
onChange={onChange ? (e) => onChange(e.currentTarget.checked) : undefined}
|
||||
checked={checked}
|
||||
disabled={!onChange}
|
||||
/>
|
||||
</li>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -359,7 +357,6 @@ const defaultRendererFactory =
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{_t(title)}</h3>
|
||||
<LazyRenderList
|
||||
element="ul"
|
||||
itemHeight={ROW_HEIGHT}
|
||||
items={rooms}
|
||||
scrollTop={scrollTop}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -113,13 +113,12 @@ const BulkRedactDialog: React.FC<Props> = (props) => {
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
<p>{_t("user_info|redact|confirm_description_1", { count, user })}</p>
|
||||
<p>{_t("user_info|redact|confirm_description_2")}</p>
|
||||
<StyledCheckbox
|
||||
description={_t("user_info|redact|confirm_keep_state_explainer")}
|
||||
checked={keepStateEvents}
|
||||
onChange={(e) => setKeepStateEvents(e.target.checked)}
|
||||
>
|
||||
<StyledCheckbox checked={keepStateEvents} onChange={(e) => setKeepStateEvents(e.target.checked)}>
|
||||
{_t("user_info|redact|confirm_keep_state_label")}
|
||||
</StyledCheckbox>
|
||||
<div className="mx_BulkRedactDialog_checkboxMicrocopy">
|
||||
{_t("user_info|redact|confirm_keep_state_explainer")}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("user_info|redact|confirm_button", { count })}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -78,6 +78,7 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
|
||||
}}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<StyledCheckbox
|
||||
checked={canContact}
|
||||
onChange={(e) => setCanContact((e.target as HTMLInputElement).checked)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -44,23 +44,22 @@ const Entry: React.FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_entry">
|
||||
<label className="mx_ManageRestrictedJoinRuleDialog_entry">
|
||||
<div>
|
||||
<div>
|
||||
{localRoom ? <RoomAvatar room={room} size="20px" /> : <RoomAvatar oobData={room} size="20px" />}
|
||||
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{room.name}</span>
|
||||
</div>
|
||||
{description && (
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_entry_description">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<StyledCheckbox
|
||||
onChange={onChange ? (e) => onChange(e.target.checked) : undefined}
|
||||
checked={checked}
|
||||
disabled={!onChange}
|
||||
description={description}
|
||||
>
|
||||
<div>
|
||||
{localRoom ? (
|
||||
<RoomAvatar role="none" room={room} size="20px" />
|
||||
) : (
|
||||
<RoomAvatar oobData={room} size="20px" />
|
||||
)}
|
||||
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{room.name}</span>
|
||||
</div>
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 React from "react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ClientWidgetApi,
|
||||
type IModalWidgetCloseRequest,
|
||||
|
||||
@@ -6,7 +6,8 @@ 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 React, { type SyntheticEvent, useRef, useState } from "react";
|
||||
import * as React from "react";
|
||||
import { type SyntheticEvent, useRef, useState } from "react";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import type Field from "../elements/Field";
|
||||
|
||||
@@ -1,95 +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, { type ChangeEventHandler, useCallback, useState } from "react";
|
||||
import { Root, Field, Label, InlineSpinner, ErrorMessage } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Markdown from "../../../Markdown";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onFinished(complete: boolean): void;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for reporting a room.
|
||||
*/
|
||||
|
||||
export const ReportRoomDialog: React.FC<IProps> = function ({ roomId, onFinished }) {
|
||||
const [error, setErr] = useState<string>();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
const [reason, setReason] = useState("");
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
const onReasonChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => setReason(e.target.value), []);
|
||||
const onCancel = useCallback(() => onFinished(sent), [sent, onFinished]);
|
||||
const onSubmit = useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await client.reportRoom(roomId, reason);
|
||||
setSent(true);
|
||||
} catch (ex) {
|
||||
if (ex instanceof Error) {
|
||||
setErr(ex.message);
|
||||
} else {
|
||||
setErr("Unknown error");
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [roomId, reason, client]);
|
||||
|
||||
const adminMessageMD = SdkConfig.getObject("report_event")?.get("admin_message_md", "adminMessageMD");
|
||||
let adminMessage: JSX.Element | undefined;
|
||||
if (adminMessageMD) {
|
||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ReportRoomDialog"
|
||||
onFinished={() => onFinished(sent)}
|
||||
title={_t("report_room|title")}
|
||||
contentId="mx_ReportEventDialog"
|
||||
>
|
||||
{sent && <p>{_t("report_room|sent")}</p>}
|
||||
{!sent && (
|
||||
<Root id="mx_ReportEventDialog" onSubmit={onSubmit}>
|
||||
<p>{_t("report_room|description")}</p>
|
||||
{adminMessage}
|
||||
<Field name="reason">
|
||||
<Label htmlFor="mx_ReportRoomDialog_reason">{_t("room_settings|permissions|ban_reason")}</Label>
|
||||
<textarea
|
||||
id="mx_ReportRoomDialog_reason"
|
||||
placeholder={_t("report_room|reason_placeholder")}
|
||||
rows={5}
|
||||
onChange={onReasonChange}
|
||||
value={reason}
|
||||
disabled={busy}
|
||||
/>
|
||||
{error ? <ErrorMessage>{error}</ErrorMessage> : null}
|
||||
</Field>
|
||||
{busy ? <InlineSpinner /> : null}
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|send_report")}
|
||||
onPrimaryButtonClick={onSubmit}
|
||||
focus={true}
|
||||
onCancel={onCancel}
|
||||
disabled={busy}
|
||||
/>
|
||||
</Root>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -45,13 +45,14 @@ const SpacePreferencesAppearanceTab: React.FC<Pick<IProps, "space">> = ({ space
|
||||
!showPeople,
|
||||
);
|
||||
}}
|
||||
description={_t("space|preferences|show_people_in_space", {
|
||||
spaceName: space.name,
|
||||
})}
|
||||
>
|
||||
{_t("common|people")}
|
||||
</StyledCheckbox>
|
||||
<SettingsSubsectionText />
|
||||
<SettingsSubsectionText>
|
||||
{_t("space|preferences|show_people_in_space", {
|
||||
spaceName: space.name,
|
||||
})}
|
||||
</SettingsSubsectionText>
|
||||
</SettingsSubsection>
|
||||
</SettingsSection>
|
||||
</SettingsTab>
|
||||
|
||||
@@ -50,11 +50,6 @@ import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserS
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
showMsc4108QrCode?: boolean;
|
||||
/**
|
||||
* If `true`, the flow for a user to reset their encryption will be shown. In this case, `initialTabId` must be `UserTab.Encryption`.
|
||||
*
|
||||
* If false or undefined, show the tab as normal.
|
||||
*/
|
||||
showResetIdentity?: boolean;
|
||||
sdkContext: SdkContextClass;
|
||||
onFinished(): void;
|
||||
@@ -97,7 +92,7 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
|
||||
export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const voipEnabled = useSettingValue(UIFeature.Voip);
|
||||
const mjolnirEnabled = useSettingValue("feature_mjolnir");
|
||||
// store these props in state as changing tabs back and forth should clear them
|
||||
// store these props in state as changing tabs back and forth should clear it
|
||||
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
|
||||
const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -100,12 +100,16 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
||||
});
|
||||
const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => {
|
||||
const text = CapabilityText.for(cap, this.props.widgetKind);
|
||||
const byline = text.byline ? (
|
||||
<span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap + i}>
|
||||
<StyledCheckbox checked={isChecked} onChange={() => this.onToggle(cap)} description={text.byline}>
|
||||
<StyledCheckbox checked={isChecked} onChange={() => this.onToggle(cap)}>
|
||||
{text.primary}
|
||||
</StyledCheckbox>
|
||||
{byline}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton, { type ButtonEvent } from "./AccessibleButton";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -28,16 +28,13 @@ interface IProps {
|
||||
|
||||
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange, className }) => {
|
||||
return (
|
||||
<div className={classnames("mx_LabelledCheckbox", className)}>
|
||||
<StyledCheckbox
|
||||
description={byline}
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
>
|
||||
<label className={classnames("mx_LabelledCheckbox", className)}>
|
||||
<StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} />
|
||||
<div className="mx_LabelledCheckbox_labels">
|
||||
<span className="mx_LabelledCheckbox_label">{label}</span>
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
{byline ? <span className="mx_LabelledCheckbox_byline">{byline}</span> : null}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ 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 React, { type ReactElement, useContext } from "react";
|
||||
import React, { type ReactElement } from "react";
|
||||
import classNames from "classnames";
|
||||
import { type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
import { LinkIcon, UserSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { usePermalink } from "../../../hooks/usePermalink";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
@@ -27,6 +28,14 @@ export enum PillType {
|
||||
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
|
||||
}
|
||||
|
||||
export const pillRoomNotifPos = (text: string | null): number => {
|
||||
return text?.indexOf("@room") ?? -1;
|
||||
};
|
||||
|
||||
export const pillRoomNotifLen = (): number => {
|
||||
return "@room".length;
|
||||
};
|
||||
|
||||
const linkIcon = <LinkIcon className="mx_Pill_LinkIcon mx_BaseAvatar" />;
|
||||
|
||||
const PillRoomAvatar: React.FC<{
|
||||
@@ -80,7 +89,6 @@ export const Pill: React.FC<PillProps> = ({
|
||||
shouldShowPillAvatar = true,
|
||||
text: customPillText,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const {
|
||||
event,
|
||||
member,
|
||||
@@ -105,7 +113,7 @@ export const Pill: React.FC<PillProps> = ({
|
||||
mx_RoomPill: type === PillType.RoomMention,
|
||||
mx_SpacePill: type === "space" || targetRoom?.isSpaceRoom(),
|
||||
mx_UserPill: type === PillType.UserMention,
|
||||
mx_UserPill_me: resourceId === cli.getUserId(),
|
||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||
mx_KeywordPill: type === PillType.Keyword,
|
||||
});
|
||||
@@ -152,24 +160,26 @@ export const Pill: React.FC<PillProps> = ({
|
||||
const isAnchor = !!inMessage && !!url;
|
||||
return (
|
||||
<bdi>
|
||||
<Tooltip
|
||||
description={resourceId ?? ""}
|
||||
open={resourceId ? undefined : false}
|
||||
placement="right"
|
||||
isTriggerInteractive={isAnchor}
|
||||
>
|
||||
{isAnchor ? (
|
||||
<a className={classes} href={url} onClick={onClick}>
|
||||
{avatar}
|
||||
<span className="mx_Pill_text">{pillText}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className={classes}>
|
||||
{avatar}
|
||||
<span className="mx_Pill_text">{pillText}</span>
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
|
||||
<Tooltip
|
||||
description={resourceId ?? ""}
|
||||
open={resourceId ? undefined : false}
|
||||
placement="right"
|
||||
isTriggerInteractive={isAnchor}
|
||||
>
|
||||
{isAnchor ? (
|
||||
<a className={classes} href={url} onClick={onClick}>
|
||||
{avatar}
|
||||
<span className="mx_Pill_text">{pillText}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className={classes}>
|
||||
{avatar}
|
||||
<span className="mx_Pill_text">{pillText}</span>
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
</MatrixClientContext.Provider>
|
||||
</bdi>
|
||||
);
|
||||
};
|
||||
|
||||