Compare commits
14 Commits
t3chguy/oi
...
andybalaam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfece0f5ee | ||
|
|
102a1ddb9e | ||
|
|
99ea51c6f2 | ||
|
|
3f1e56b715 | ||
|
|
f3653abe92 | ||
|
|
a6e8d512d0 | ||
|
|
13c4ab2cf4 | ||
|
|
74da64db63 | ||
|
|
e5d37a324d | ||
|
|
d0c1610bd2 | ||
|
|
64e2a843c3 | ||
|
|
fba59381a0 | ||
|
|
e1970df704 | ||
|
|
b54122884c |
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
|||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"Sender's verified identity has changed",
|
"Sender's verified identity was reset",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
|||||||
/* should show an error for a message from a previously verified device */
|
/* should show an error for a message from a previously verified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,4 +73,33 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await revokeAccessTokenPromise;
|
await revokeAccessTokenPromise;
|
||||||
await revokeRefreshTokenPromise;
|
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 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test";
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
const ROOM_NAME = "Test room";
|
const ROOM_NAME = "Test room";
|
||||||
const ROOM_NAME_LONG =
|
const ROOM_NAME_LONG =
|
||||||
@@ -133,6 +134,17 @@ test.describe("RightPanel", () => {
|
|||||||
await page.getByLabel("Room info").nth(1).click();
|
await page.getByLabel("Room info").nth(1).click();
|
||||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
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", () => {
|
test.describe("in spaces", () => {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
|||||||
const OLD_NAME = "Alan";
|
const OLD_NAME = "Alan";
|
||||||
const NEW_NAME = "Alan (away)";
|
const NEW_NAME = "Alan (away)";
|
||||||
|
|
||||||
|
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||||
|
|
||||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||||
};
|
};
|
||||||
@@ -916,7 +918,27 @@ test.describe("Timeline", () => {
|
|||||||
await page.getByRole("button", { name: "Hide" }).click();
|
await page.getByRole("button", { name: "Hide" }).click();
|
||||||
|
|
||||||
// Check that the image is now hidden.
|
// Check that the image is now hidden.
|
||||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
playwright/sample-files/5secvid.webm
Normal file
BIN
playwright/sample-files/5secvid.webm
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 957 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 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";
|
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||||
|
|
||||||
const TAG = "develop@sha256:4285f51332a658ba6d4871b04d33f49261e6118e751d70fd2894aca97bd587c3";
|
const TAG = "develop@sha256:d19854a3dbbb4d5d24d84767d17e1a623181ae5f2bdda3505819c05a8d3c8611";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
||||||
@import "./views/dialogs/_PollCreateDialog.pcss";
|
@import "./views/dialogs/_PollCreateDialog.pcss";
|
||||||
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
||||||
|
@import "./views/dialogs/_ReportRoomDialog.pcss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
||||||
@@ -226,6 +227,7 @@
|
|||||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||||
@import "./views/messages/_EventTileBubble.pcss";
|
@import "./views/messages/_EventTileBubble.pcss";
|
||||||
@import "./views/messages/_HiddenBody.pcss";
|
@import "./views/messages/_HiddenBody.pcss";
|
||||||
|
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
|
||||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||||
@import "./views/messages/_MEmoteBody.pcss";
|
@import "./views/messages/_MEmoteBody.pcss";
|
||||||
|
|||||||
16
res/css/views/dialogs/_ReportRoomDialog.pcss
Normal file
16
res/css/views/dialogs/_ReportRoomDialog.pcss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.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,39 +79,3 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
color: $imagebody-giflabel-color;
|
color: $imagebody-giflabel-color;
|
||||||
pointer-events: none;
|
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 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
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
|
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;
|
margin: $spacing-12 0 $spacing-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_leave {
|
.mx_RoomSummaryCard_bottomOptions {
|
||||||
margin: 0 0 var(--cpd-space-8x);
|
margin: 0 0 var(--cpd-space-8x);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ interface ILoadSessionOpts {
|
|||||||
ignoreGuest?: boolean;
|
ignoreGuest?: boolean;
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
fragmentQueryParams?: QueryDict;
|
fragmentQueryParams?: QueryDict;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,7 +197,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
|
|
||||||
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
||||||
logger.log("Using guest access credentials");
|
logger.log("Using guest access credentials");
|
||||||
return doSetLoggedIn(
|
await doSetLoggedIn(
|
||||||
{
|
{
|
||||||
userId: fragmentQueryParams.guest_user_id as string,
|
userId: fragmentQueryParams.guest_user_id as string,
|
||||||
accessToken: fragmentQueryParams.guest_access_token as string,
|
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||||
@@ -206,7 +207,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
).then(() => true);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const success = await restoreSessionFromStorage({
|
const success = await restoreSessionFromStorage({
|
||||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||||
@@ -225,6 +227,11 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
// fall back to welcome screen
|
// fall back to welcome screen
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} 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 (e instanceof AbortLoginAndRebuildStorage) {
|
||||||
// If we're aborting login because of a storage inconsistency, we don't
|
// 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.
|
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||||
@@ -236,7 +243,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleLoadSessionFailure(e);
|
return handleLoadSessionFailure(e, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +413,39 @@ 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.
|
* Called after a successful token login or OIDC authorization.
|
||||||
* Clear storage then save new credentials in storage
|
* Clear storage then save new credentials in storage
|
||||||
@@ -413,6 +453,8 @@ export function attemptTokenLogin(
|
|||||||
*/
|
*/
|
||||||
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
||||||
await clearStorage();
|
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);
|
await persistCredentials(credentials);
|
||||||
|
|
||||||
// remember that we just logged in
|
// remember that we just logged in
|
||||||
@@ -621,7 +663,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise<boolean> {
|
||||||
logger.error("Unable to load session", e);
|
logger.error("Unable to load session", e);
|
||||||
|
|
||||||
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
||||||
@@ -636,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try, try again
|
// try, try again
|
||||||
return loadSession();
|
return loadSession(loadSessionOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -655,18 +697,8 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
|||||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||||
credentials.freshLogin = true;
|
credentials.freshLogin = true;
|
||||||
stopMatrixClient();
|
stopMatrixClient();
|
||||||
const pickleKey =
|
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||||
credentials.userId && credentials.deviceId
|
return doSetLoggedIn(credentials, true, true);
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1124,12 +1156,13 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
|
|||||||
baseUrl: hsUrl,
|
baseUrl: hsUrl,
|
||||||
accessToken,
|
accessToken,
|
||||||
});
|
});
|
||||||
const { user_id: userId } = await tempClient.whoami();
|
const { user_id: userId, device_id: deviceId } = await tempClient.whoami();
|
||||||
await doSetLoggedIn(
|
await doSetLoggedIn(
|
||||||
{
|
{
|
||||||
homeserverUrl: hsUrl,
|
homeserverUrl: hsUrl,
|
||||||
accessToken,
|
accessToken,
|
||||||
userId,
|
userId,
|
||||||
|
deviceId,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -238,25 +238,25 @@ export function determineUnreadState(
|
|||||||
room?: Room,
|
room?: Room,
|
||||||
threadId?: string,
|
threadId?: string,
|
||||||
includeThreads?: boolean,
|
includeThreads?: boolean,
|
||||||
): { level: NotificationLevel; symbol: string | null; count: number } {
|
): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getUnsentMessages(room, threadId).length > 0) {
|
if (getUnsentMessages(room, threadId).length > 0) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent };
|
return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
||||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const redNotifs = getUnreadNotificationCount(
|
const redNotifs = getUnreadNotificationCount(
|
||||||
@@ -269,12 +269,12 @@ export function determineUnreadState(
|
|||||||
|
|
||||||
const trueCount = greyNotifs || redNotifs;
|
const trueCount = greyNotifs || redNotifs;
|
||||||
if (redNotifs > 0) {
|
if (redNotifs > 0) {
|
||||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
|
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const markedUnreadState = getMarkedUnreadState(room);
|
const markedUnreadState = getMarkedUnreadState(room);
|
||||||
if (greyNotifs > 0 || markedUnreadState) {
|
if (greyNotifs > 0 || markedUnreadState) {
|
||||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
|
return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
||||||
@@ -293,5 +293,6 @@ export function determineUnreadState(
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: trueCount,
|
count: trueCount,
|
||||||
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
private themeWatcher?: ThemeWatcher;
|
private themeWatcher?: ThemeWatcher;
|
||||||
private fontWatcher?: FontWatcher;
|
private fontWatcher?: FontWatcher;
|
||||||
private readonly stores: SdkContextClass;
|
private readonly stores: SdkContextClass;
|
||||||
|
private loadSessionAbortController = new AbortController();
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -327,7 +328,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
|
// 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
|
// will be sent out to say that, triggering this MatrixChat to show the soft
|
||||||
// logout page.
|
// logout page.
|
||||||
Lifecycle.loadSession();
|
Lifecycle.loadSession({ abortSignal: this.loadSessionAbortController.signal });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,6 +553,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
||||||
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
|
abortSignal: this.loadSessionAbortController.signal,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((loadedSession) => {
|
.then((loadedSession) => {
|
||||||
@@ -1565,26 +1567,33 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
cli.on(HttpApiEvent.SessionLoggedOut, (errObj) => {
|
||||||
|
this.loadSessionAbortController.abort(errObj);
|
||||||
|
this.loadSessionAbortController = new AbortController();
|
||||||
|
|
||||||
if (Lifecycle.isLoggingOut()) return;
|
if (Lifecycle.isLoggingOut()) return;
|
||||||
|
|
||||||
// A modal might have been open when we were logged out by the server
|
// A modal might have been open when we were logged out by the server
|
||||||
Modal.forceCloseAllModals();
|
Modal.forceCloseAllModals();
|
||||||
|
|
||||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
|
if (errObj.httpStatus === 401 && errObj.data?.["soft_logout"]) {
|
||||||
logger.warn("Soft logout issued by server - avoiding data deletion");
|
logger.warn("Soft logout issued by server - avoiding data deletion");
|
||||||
Lifecycle.softLogout();
|
Lifecycle.softLogout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dis.dispatch(
|
||||||
|
{
|
||||||
|
action: "logout",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The above dispatch closes all modals, so open the modal after calling it synchronously
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("auth|session_logged_out_title"),
|
title: _t("auth|session_logged_out_title"),
|
||||||
description: _t("auth|session_logged_out_description"),
|
description: _t("auth|session_logged_out_description"),
|
||||||
});
|
});
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: "logout",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
|
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
|
||||||
Modal.createDialog(
|
Modal.createDialog(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
|
|||||||
import dispatcher from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
import { useIndexForActiveRoom } from "./useIndexForActiveRoom";
|
import { useStickyRoomList } from "./useStickyRoomList";
|
||||||
|
|
||||||
export interface RoomListViewState {
|
export interface RoomListViewState {
|
||||||
/**
|
/**
|
||||||
@@ -97,8 +97,14 @@ export interface RoomListViewState {
|
|||||||
*/
|
*/
|
||||||
export function useRoomListViewModel(): RoomListViewState {
|
export function useRoomListViewModel(): RoomListViewState {
|
||||||
const matrixClient = useMatrixClientContext();
|
const matrixClient = useMatrixClientContext();
|
||||||
const { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter } =
|
const {
|
||||||
useFilteredRooms();
|
primaryFilters,
|
||||||
|
activePrimaryFilter,
|
||||||
|
rooms: filteredRooms,
|
||||||
|
activateSecondaryFilter,
|
||||||
|
activeSecondaryFilter,
|
||||||
|
} = useFilteredRooms();
|
||||||
|
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
|
||||||
|
|
||||||
const currentSpace = useEventEmitterState<Room | null>(
|
const currentSpace = useEventEmitterState<Room | null>(
|
||||||
SpaceStore.instance,
|
SpaceStore.instance,
|
||||||
@@ -107,7 +113,6 @@ export function useRoomListViewModel(): RoomListViewState {
|
|||||||
);
|
);
|
||||||
const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace);
|
const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace);
|
||||||
|
|
||||||
const activeIndex = useIndexForActiveRoom(rooms);
|
|
||||||
const { activeSortOption, sort } = useSorter();
|
const { activeSortOption, sort } = useSorter();
|
||||||
const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle();
|
const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle();
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks the index of the active room in the given array of rooms.
|
|
||||||
* @param rooms list of rooms
|
|
||||||
* @returns index of the active room or undefined otherwise.
|
|
||||||
*/
|
|
||||||
export function useIndexForActiveRoom(rooms: Room[]): number | undefined {
|
|
||||||
const [index, setIndex] = useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
const calculateIndex = useCallback(
|
|
||||||
(newRoomId?: string) => {
|
|
||||||
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
|
|
||||||
const index = rooms.findIndex((room) => room.roomId === activeRoomId);
|
|
||||||
setIndex(index === -1 ? undefined : index);
|
|
||||||
},
|
|
||||||
[rooms],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-calculate the index when the active room has changed.
|
|
||||||
useDispatcher(dispatcher, (payload) => {
|
|
||||||
if (payload.action === Action.ActiveRoomChanged) calculateIndex(payload.newRoomId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-calculate the index when the list of rooms has changed.
|
|
||||||
useEffect(() => {
|
|
||||||
calculateIndex();
|
|
||||||
}, [calculateIndex, rooms]);
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
117
src/components/viewmodels/roomlist/useStickyRoomList.tsx
Normal file
117
src/components/viewmodels/roomlist/useStickyRoomList.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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 };
|
||||||
|
}
|
||||||
95
src/components/views/dialogs/ReportRoomDialog.tsx
Normal file
95
src/components/views/dialogs/ReportRoomDialog.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/components/views/messages/HiddenMediaPlaceholder.tsx
Normal file
24
src/components/views/messages/HiddenMediaPlaceholder.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
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 PropsWithChildren, type MouseEventHandler } from "react";
|
||||||
|
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HiddenMediaPlaceholder: React.FunctionComponent<PropsWithChildren<IProps>> = ({ onClick, children }) => {
|
||||||
|
return (
|
||||||
|
<button onClick={onClick} className="mx_HiddenMediaPlaceholder">
|
||||||
|
<div>
|
||||||
|
<VisibilityOnIcon />
|
||||||
|
<span>{children}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -33,6 +33,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
|
|||||||
import { createReconnectedListener } from "../../../utils/connection";
|
import { createReconnectedListener } from "../../../utils/connection";
|
||||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||||
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
|
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
|
||||||
|
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
|
||||||
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
enum Placeholder {
|
enum Placeholder {
|
||||||
@@ -95,7 +96,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
if (ev.button === 0 && !ev.metaKey) {
|
if (ev.button === 0 && !ev.metaKey) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this.props.mediaVisible) {
|
if (!this.props.mediaVisible) {
|
||||||
this.props.setMediaVisible?.(true);
|
this.props.setMediaVisible(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +438,11 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
if (!this.state.loadedImageDimensions) {
|
if (!this.state.loadedImageDimensions) {
|
||||||
let imageElement: JSX.Element;
|
let imageElement: JSX.Element;
|
||||||
if (!this.props.mediaVisible) {
|
if (!this.props.mediaVisible) {
|
||||||
imageElement = <HiddenImagePlaceholder />;
|
imageElement = (
|
||||||
|
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||||
|
{_t("timeline|m.image|show_image")}
|
||||||
|
</HiddenMediaPlaceholder>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
imageElement = (
|
imageElement = (
|
||||||
<img
|
<img
|
||||||
@@ -507,7 +512,13 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.props.mediaVisible) {
|
if (!this.props.mediaVisible) {
|
||||||
img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
|
img = (
|
||||||
|
<div style={{ width: maxWidth, height: maxHeight }}>
|
||||||
|
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||||
|
{_t("timeline|m.image|show_image")}
|
||||||
|
</HiddenMediaPlaceholder>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +574,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HACK: This div fills out space while the image loads, to prevent scroll jumps */}
|
{/* HACK: This div fills out space while the image loads, to prevent scroll jumps */}
|
||||||
{!this.props.forExport && !this.state.imgLoaded && (
|
{!this.props.forExport && !this.state.imgLoaded && !placeholder && (
|
||||||
<div style={{ height: maxHeight, width: maxWidth }} />
|
<div style={{ height: maxHeight, width: maxWidth }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -596,12 +607,6 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (!this.props.mediaVisible) {
|
|
||||||
return (
|
|
||||||
<div role="button" onClick={this.onClick}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
@@ -680,24 +685,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaceholderIProps {
|
// Wrap MImageBody component so we can use a hook here.
|
||||||
maxWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
|
|
||||||
return (
|
|
||||||
<div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
|
|
||||||
<div className="mx_HiddenImagePlaceholder_button">
|
|
||||||
<span className="mx_HiddenImagePlaceholder_eye" />
|
|
||||||
<span>{_t("timeline|m.image|show_image")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MImageBody: React.FC<IBodyProps> = (props) => {
|
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import MFileBody from "./MFileBody";
|
|||||||
import { type ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
|
import { type ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||||
|
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl: string | null;
|
decryptedUrl: string | null;
|
||||||
@@ -32,7 +34,19 @@ interface IState {
|
|||||||
blurhashUrl: string | null;
|
blurhashUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
|
interface IProps extends IBodyProps {
|
||||||
|
/**
|
||||||
|
* Should the media be behind a preview.
|
||||||
|
*/
|
||||||
|
mediaVisible: boolean;
|
||||||
|
/**
|
||||||
|
* Set the visibility of the media event.
|
||||||
|
* @param visible Should the event be visible.
|
||||||
|
*/
|
||||||
|
setMediaVisible: (visible: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MVideoBodyInner extends React.PureComponent<IProps, IState> {
|
||||||
public static contextType = RoomContext;
|
public static contextType = RoomContext;
|
||||||
declare public context: React.ContextType<typeof RoomContext>;
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
@@ -49,6 +63,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||||||
blurhashUrl: null,
|
blurhashUrl: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onClick = (): void => {
|
||||||
|
this.props.setMediaVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
private getContentUrl(): string | undefined {
|
private getContentUrl(): string | undefined {
|
||||||
const content = this.props.mxEvent.getContent<MediaEventContent>();
|
const content = this.props.mxEvent.getContent<MediaEventContent>();
|
||||||
// During export, the content url will point to the MSC, which will later point to a local url
|
// During export, the content url will point to the MSC, which will later point to a local url
|
||||||
@@ -120,11 +138,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount(): Promise<void> {
|
private async downloadVideo(): Promise<void> {
|
||||||
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
|
|
||||||
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loadBlurhash();
|
this.loadBlurhash();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -174,6 +188,23 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async componentDidMount(): Promise<void> {
|
||||||
|
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
|
||||||
|
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not attempt to load the media if we do not want to show previews here.
|
||||||
|
if (this.props.mediaVisible) {
|
||||||
|
await this.downloadVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidUpdate(prevProps: Readonly<IProps>): Promise<void> {
|
||||||
|
if (!prevProps.mediaVisible && this.props.mediaVisible) {
|
||||||
|
await this.downloadVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
SettingsStore.unwatchSetting(this.sizeWatcher);
|
SettingsStore.unwatchSetting(this.sizeWatcher);
|
||||||
}
|
}
|
||||||
@@ -244,6 +275,22 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users may not even want to show a poster, so instead show a preview button.
|
||||||
|
if (!this.props.mediaVisible) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MVideoBody">
|
||||||
|
<div
|
||||||
|
className="mx_MVideoBody_container"
|
||||||
|
style={{ width: maxWidth, height: maxHeight, aspectRatio }}
|
||||||
|
>
|
||||||
|
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||||
|
{_t("timeline|m.video|show_video")}
|
||||||
|
</HiddenMediaPlaceholder>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||||
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
@@ -294,3 +341,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap MVideoBody component so we can use a hook here.
|
||||||
|
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
||||||
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||||
|
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MVideoBody;
|
||||||
|
|||||||
@@ -536,9 +536,11 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||||||
mediaEventHelperGet={() => this.props.getTile()?.getMediaHelper?.()}
|
mediaEventHelperGet={() => this.props.getTile()?.getMediaHelper?.()}
|
||||||
key="download"
|
key="download"
|
||||||
/>,
|
/>,
|
||||||
<HideActionButton mxEvent={this.props.mxEvent} key="hide" />,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (MediaEventHelper.canHide(this.props.mxEvent)) {
|
||||||
|
toolbarOpts.splice(0, 0, <HideActionButton mxEvent={this.props.mxEvent} key="hide" />);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
// Show thread icon even for deleted messages, but only within main timeline
|
// Show thread icon even for deleted messages, but only within main timeline
|
||||||
this.context.timelineRenderingType === TimelineRenderingType.Room &&
|
this.context.timelineRenderingType === TimelineRenderingType.Room &&
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -35,7 +35,8 @@ import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin";
|
|||||||
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
|
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
|
||||||
import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off";
|
import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off";
|
||||||
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
||||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||||
|
import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
||||||
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
||||||
import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
|
|||||||
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
|
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
|
||||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
||||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||||
|
import { ReportRoomDialog } from "../dialogs/ReportRoomDialog.tsx";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -231,6 +233,11 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const onReportRoomClick = (): void => {
|
||||||
|
Modal.createDialog(ReportRoomDialog, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||||
const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType");
|
const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType");
|
||||||
@@ -320,7 +327,7 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||||||
|
|
||||||
{e2eStatus === E2EStatus.Warning && (
|
{e2eStatus === E2EStatus.Warning && (
|
||||||
<Badge kind="red">
|
<Badge kind="red">
|
||||||
<ErrorIcon width="1em" />
|
<ErrorSolidIcon width="1em" />
|
||||||
{_t("common|not_trusted")}
|
{_t("common|not_trusted")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -439,14 +446,21 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||||||
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
|
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<div className="mx_RoomSummaryCard_bottomOptions">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="mx_RoomSummaryCard_leave"
|
className="mx_RoomSummaryCard_leave"
|
||||||
Icon={LeaveIcon}
|
Icon={LeaveIcon}
|
||||||
kind="critical"
|
kind="critical"
|
||||||
label={_t("action|leave_room")}
|
label={_t("action|leave_room")}
|
||||||
onSelect={onLeaveRoomClick}
|
onSelect={onLeaveRoomClick}
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
Icon={ErrorIcon}
|
||||||
|
kind="critical"
|
||||||
|
label={_t("action|report_room")}
|
||||||
|
onSelect={onReportRoomClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface UserIdentityWarningProps {
|
|||||||
/**
|
/**
|
||||||
* Displays a banner warning when there is an issue with a user's identity.
|
* Displays a banner warning when there is an issue with a user's identity.
|
||||||
*
|
*
|
||||||
* Warns when an unverified user's identity has changed, and gives the user a
|
* Warns when an unverified user's identity was reset, and gives the user a
|
||||||
* button to acknowledge the change.
|
* button to acknowledge the change.
|
||||||
*/
|
*/
|
||||||
export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => {
|
export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => {
|
||||||
@@ -104,7 +104,7 @@ function getTitleAndAction(prompt: ViolationPrompt): [title: React.ReactNode, ac
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
action = _t("action|ok");
|
action = _t("action|dismiss");
|
||||||
}
|
}
|
||||||
return [title, action];
|
return [title, action];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"go": "Ok",
|
"go": "Ok",
|
||||||
"go_back": "Zpět",
|
"go_back": "Zpět",
|
||||||
"got_it": "Rozumím",
|
"got_it": "Rozumím",
|
||||||
|
"hide": "Skrýt",
|
||||||
"hide_advanced": "Skrýt pokročilé možnosti",
|
"hide_advanced": "Skrýt pokročilé možnosti",
|
||||||
"hold": "Podržet",
|
"hold": "Podržet",
|
||||||
"ignore": "Ignorovat",
|
"ignore": "Ignorovat",
|
||||||
@@ -952,8 +953,8 @@
|
|||||||
"warning": "Pokud jste nenastavili nový způsob obnovy vy, mohou se pokoušet k vašemu účtu dostat útočníci. Změňte si raději ihned heslo a nastavte nový způsob obnovy v Nastavení."
|
"warning": "Pokud jste nenastavili nový způsob obnovy vy, mohou se pokoušet k vašemu účtu dostat útočníci. Změňte si raději ihned heslo a nastavte nový způsob obnovy v Nastavení."
|
||||||
},
|
},
|
||||||
"not_supported": "<nepodporováno>",
|
"not_supported": "<nepodporováno>",
|
||||||
"pinned_identity_changed": "%(displayName)s(<b>%(userId)s</b>) se zřejmě změnil. <a>Více informací</a>",
|
"pinned_identity_changed": "Identita %(displayName)s (<b>%(userId)s</b>) byla změněna. <a>Další informace</a>",
|
||||||
"pinned_identity_changed_no_displayname": "Identita <b>%(userId)s</b> se zřejmě změnila. <a>Další informace</a>",
|
"pinned_identity_changed_no_displayname": "Identita <b>%(userId)s</b> byla změněna. <a>Další informace</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "Tato relace zjistila, že byla odstraněna vaše bezpečnostní fráze a klíč pro zabezpečené zprávy.",
|
"description_1": "Tato relace zjistila, že byla odstraněna vaše bezpečnostní fráze a klíč pro zabezpečené zprávy.",
|
||||||
"description_2": "Pokud se vám to stalo neúmyslně, můžete znovu nastavit zálohu zpráv pro tuto relaci. To znovu zašifruje historii zpráv novým způsobem.",
|
"description_2": "Pokud se vám to stalo neúmyslně, můžete znovu nastavit zálohu zpráv pro tuto relaci. To znovu zašifruje historii zpráv novým způsobem.",
|
||||||
@@ -1065,8 +1066,8 @@
|
|||||||
"waiting_other_user": "Čekám až nás %(displayName)s ověří…"
|
"waiting_other_user": "Čekám až nás %(displayName)s ověří…"
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Žádost ověření",
|
"verification_requested_toast_title": "Žádost ověření",
|
||||||
"verified_identity_changed": "Ověřená identita %(displayName)s (<b>%(userId)s</b>) se změnila. <a>Další informace</a>",
|
"verified_identity_changed": "Ověřená identita %(displayName)s (<b>%(userId)s</b>) byla změněna. <a>Další informace</a>",
|
||||||
"verified_identity_changed_no_displayname": "Ověřená identita uživatele <b>%(userId)s</b> se změnila. <a>Další informace</a>",
|
"verified_identity_changed_no_displayname": "Ověřená identita uživatele <b>%(userId)s</b> byla změněna. <a>Další informace</a>",
|
||||||
"verify_toast_description": "Ostatní uživatelé této relaci nemusí věřit",
|
"verify_toast_description": "Ostatní uživatelé této relaci nemusí věřit",
|
||||||
"verify_toast_title": "Ověřit tuto relaci",
|
"verify_toast_title": "Ověřit tuto relaci",
|
||||||
"withdraw_verification_action": "Zrušit ověření"
|
"withdraw_verification_action": "Zrušit ověření"
|
||||||
@@ -2101,6 +2102,19 @@
|
|||||||
"add_space_label": "Přidat prostor",
|
"add_space_label": "Přidat prostor",
|
||||||
"breadcrumbs_empty": "Žádné nedávno navštívené místnosti",
|
"breadcrumbs_empty": "Žádné nedávno navštívené místnosti",
|
||||||
"breadcrumbs_label": "Nedávno navštívené místnosti",
|
"breadcrumbs_label": "Nedávno navštívené místnosti",
|
||||||
|
"empty": {
|
||||||
|
"no_chats": "Zatím žádné chaty",
|
||||||
|
"no_chats_description": "Začněte tím, že někomu pošlete zprávu nebo vytvoříte místnost",
|
||||||
|
"no_chats_description_no_room_rights": "Začněte tím, že někomu pošlete zprávu",
|
||||||
|
"no_favourites": "Zatím nemáte oblíbený chat",
|
||||||
|
"no_favourites_description": "Chat si můžete přidat do oblíbených v nastavení chatu",
|
||||||
|
"no_people": "Zatím s nikým nemáte přímé chaty",
|
||||||
|
"no_people_description": "Můžete zrušit filtry, abyste viděli své další chaty",
|
||||||
|
"no_rooms": "Ještě nejste v žádné místnosti",
|
||||||
|
"no_rooms_description": "Můžete zrušit filtry, abyste viděli své další chaty",
|
||||||
|
"no_unread": "Gratulujeme! Nemáte žádné nepřečtené zprávy",
|
||||||
|
"show_chats": "Zobrazit všechny chaty"
|
||||||
|
},
|
||||||
"failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti",
|
"failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti",
|
||||||
"failed_remove_tag": "Nepodařilo se odstranit štítek %(tagName)s z místnosti",
|
"failed_remove_tag": "Nepodařilo se odstranit štítek %(tagName)s z místnosti",
|
||||||
"failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy",
|
"failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy",
|
||||||
@@ -3139,6 +3153,7 @@
|
|||||||
"view": "Zobrazí místnost s danou adresou",
|
"view": "Zobrazí místnost s danou adresou",
|
||||||
"whois": "Zobrazuje informace o uživateli"
|
"whois": "Zobrazuje informace o uživateli"
|
||||||
},
|
},
|
||||||
|
"sliding_sync_legacy_no_longer_supported": "Starší klouzavá synchronizace již není podporována: odhlaste se a znovu přihlaste, abyste povolili nový způsob posuvné synchronizace",
|
||||||
"space": {
|
"space": {
|
||||||
"add_existing_room_space": {
|
"add_existing_room_space": {
|
||||||
"create": "Chcete místo toho přidat novou místnost?",
|
"create": "Chcete místo toho přidat novou místnost?",
|
||||||
@@ -3352,7 +3367,7 @@
|
|||||||
"historical_event_no_key_backup": "Historické zprávy nejsou v tomto zařízení k dispozici",
|
"historical_event_no_key_backup": "Historické zprávy nejsou v tomto zařízení k dispozici",
|
||||||
"historical_event_unverified_device": "Pro přístup k historickým zprávám je třeba toto zařízení ověřit.",
|
"historical_event_unverified_device": "Pro přístup k historickým zprávám je třeba toto zařízení ověřit.",
|
||||||
"historical_event_user_not_joined": "K této zprávě nemáte přístup",
|
"historical_event_user_not_joined": "K této zprávě nemáte přístup",
|
||||||
"sender_identity_previously_verified": "Ověřená identita se změnila",
|
"sender_identity_previously_verified": "Ověřená identita odesílatele se změnila",
|
||||||
"sender_unsigned_device": "Šifrováno zařízením, které nebylo ověřeno jeho majitelem.",
|
"sender_unsigned_device": "Šifrováno zařízením, které nebylo ověřeno jeho majitelem.",
|
||||||
"unable_to_decrypt": "Zprávu nelze dešifrovat"
|
"unable_to_decrypt": "Zprávu nelze dešifrovat"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"go": "Los",
|
"go": "Los",
|
||||||
"go_back": "Zurück",
|
"go_back": "Zurück",
|
||||||
"got_it": "Verstanden",
|
"got_it": "Verstanden",
|
||||||
|
"hide": "Verstecken",
|
||||||
"hide_advanced": "Erweiterte Einstellungen ausblenden",
|
"hide_advanced": "Erweiterte Einstellungen ausblenden",
|
||||||
"hold": "Halten",
|
"hold": "Halten",
|
||||||
"ignore": "Blockieren",
|
"ignore": "Blockieren",
|
||||||
@@ -455,6 +456,7 @@
|
|||||||
"access_token": "Zugriffstoken",
|
"access_token": "Zugriffstoken",
|
||||||
"accessibility": "Barrierefreiheit",
|
"accessibility": "Barrierefreiheit",
|
||||||
"advanced": "Erweitert",
|
"advanced": "Erweitert",
|
||||||
|
"all_chats": "Alle Chats",
|
||||||
"analytics": "Analysedaten",
|
"analytics": "Analysedaten",
|
||||||
"and_n_others": {
|
"and_n_others": {
|
||||||
"other": "und %(count)s weitere …",
|
"other": "und %(count)s weitere …",
|
||||||
@@ -950,8 +952,8 @@
|
|||||||
"warning": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein Angreifer möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest."
|
"warning": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein Angreifer möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest."
|
||||||
},
|
},
|
||||||
"not_supported": "<nicht unterstützt>",
|
"not_supported": "<nicht unterstützt>",
|
||||||
"pinned_identity_changed": "Die Identität von %(displayName)s (<b>%(userId)s)</b>) hat sich geändert. <a>Mehr erfahren</a>",
|
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) Identität wurde zurückgesetzt. <a>Mehr erfahren </a>",
|
||||||
"pinned_identity_changed_no_displayname": "Die Identität von <b>%(userId)s</b> hat sich geändert. <a>Mehr erfahren</a>",
|
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>Die Identität wurde zurückgesetzt. <a>Erfahre mehr </a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.",
|
"description_1": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.",
|
||||||
"description_2": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.",
|
"description_2": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.",
|
||||||
@@ -1063,8 +1065,8 @@
|
|||||||
"waiting_other_user": "Warte darauf, dass %(displayName)s bestätigt…"
|
"waiting_other_user": "Warte darauf, dass %(displayName)s bestätigt…"
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Verifizierung angefragt",
|
"verification_requested_toast_title": "Verifizierung angefragt",
|
||||||
"verified_identity_changed": "Die verifizierte Identität von %(displayName)s (<b>%(userId)s</b>) hat sich geändert. <a>Weitere Informationen</a>",
|
"verified_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) Identität wurde zurückgesetzt. <a>Erfahren Sie mehr </a>",
|
||||||
"verified_identity_changed_no_displayname": "Die Identität von <b>%(userId)</b> hat sich geändert. <a>Weitere Informationen</a>",
|
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>Die Identität wurde zurückgesetzt. <a>Erfahren Sie mehr </a>",
|
||||||
"verify_toast_description": "Andere Benutzer vertrauen ihr vielleicht nicht",
|
"verify_toast_description": "Andere Benutzer vertrauen ihr vielleicht nicht",
|
||||||
"verify_toast_title": "Sitzung verifizieren",
|
"verify_toast_title": "Sitzung verifizieren",
|
||||||
"withdraw_verification_action": "Verifizierung zurückziehen"
|
"withdraw_verification_action": "Verifizierung zurückziehen"
|
||||||
@@ -2094,6 +2096,19 @@
|
|||||||
"add_space_label": "Space hinzufügen",
|
"add_space_label": "Space hinzufügen",
|
||||||
"breadcrumbs_empty": "Keine kürzlich besuchten Räume",
|
"breadcrumbs_empty": "Keine kürzlich besuchten Räume",
|
||||||
"breadcrumbs_label": "Kürzlich besuchte Räume",
|
"breadcrumbs_label": "Kürzlich besuchte Räume",
|
||||||
|
"empty": {
|
||||||
|
"no_chats": "Noch keine Chats",
|
||||||
|
"no_chats_description": "Beginnen Sie, indem Sie jemandem eine Nachricht senden oder einen Raum erstellen",
|
||||||
|
"no_chats_description_no_room_rights": "Beginnen Sie damit, jemandem eine Nachricht zu senden",
|
||||||
|
"no_favourites": "Sie haben noch keinen Lieblingschat",
|
||||||
|
"no_favourites_description": "Sie können einen Chat in den Chat-Einstellungen zu Ihren Favoriten hinzufügen",
|
||||||
|
"no_people": "Sie haben noch keine direkten Chats",
|
||||||
|
"no_people_description": "Sie können Filter deaktivieren, um Ihre anderen Chats anzuzeigen",
|
||||||
|
"no_rooms": "Sie sind noch in keinem Raum",
|
||||||
|
"no_rooms_description": "Sie können Filter deaktivieren, um Ihre anderen Chats anzuzeigen",
|
||||||
|
"no_unread": "Glückwunsch! Sie haben keine ungelesenen Nachrichten",
|
||||||
|
"show_chats": "Alle Chats anzeigen"
|
||||||
|
},
|
||||||
"failed_add_tag": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",
|
"failed_add_tag": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",
|
||||||
"failed_remove_tag": "Entfernen der Raum-Kennzeichnung %(tagName)s fehlgeschlagen",
|
"failed_remove_tag": "Entfernen der Raum-Kennzeichnung %(tagName)s fehlgeschlagen",
|
||||||
"failed_set_dm_tag": "Fehler beim Setzen der Nachrichtenmarkierung",
|
"failed_set_dm_tag": "Fehler beim Setzen der Nachrichtenmarkierung",
|
||||||
@@ -2110,6 +2125,14 @@
|
|||||||
"other": "Betrete %(count)s Räume"
|
"other": "Betrete %(count)s Räume"
|
||||||
},
|
},
|
||||||
"list_title": "Raumliste",
|
"list_title": "Raumliste",
|
||||||
|
"more_options": {
|
||||||
|
"copy_link": "Raumlink kopieren",
|
||||||
|
"favourited": "Favorisiert",
|
||||||
|
"leave_room": "Raum verlassen",
|
||||||
|
"low_priority": "Niedrige Priorität",
|
||||||
|
"mark_read": "Als gelesen markieren",
|
||||||
|
"mark_unread": "Als ungelesen markieren"
|
||||||
|
},
|
||||||
"notification_options": "Benachrichtigungsoptionen",
|
"notification_options": "Benachrichtigungsoptionen",
|
||||||
"open_space_menu": "Menü „Raum öffnen“",
|
"open_space_menu": "Menü „Raum öffnen“",
|
||||||
"primary_filters": "Filter für die Raumliste",
|
"primary_filters": "Filter für die Raumliste",
|
||||||
@@ -2118,6 +2141,7 @@
|
|||||||
"other": "Entferne Nachrichten in %(count)s Räumen"
|
"other": "Entferne Nachrichten in %(count)s Räumen"
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
|
"more_options": "Weitere Optionen",
|
||||||
"open_room": "Raum öffnen %(roomName)s"
|
"open_room": "Raum öffnen %(roomName)s"
|
||||||
},
|
},
|
||||||
"show_less": "Weniger anzeigen",
|
"show_less": "Weniger anzeigen",
|
||||||
@@ -3123,6 +3147,7 @@
|
|||||||
"view": "Raum mit angegebener Adresse betrachten",
|
"view": "Raum mit angegebener Adresse betrachten",
|
||||||
"whois": "Zeigt Informationen über Benutzer"
|
"whois": "Zeigt Informationen über Benutzer"
|
||||||
},
|
},
|
||||||
|
"sliding_sync_legacy_no_longer_supported": "Legacy Sliding Sync wird nicht mehr unterstützt: Bitte melden Sie sich ab und wieder an, um das neue Sliding Sync-Flag zu aktivieren",
|
||||||
"space": {
|
"space": {
|
||||||
"add_existing_room_space": {
|
"add_existing_room_space": {
|
||||||
"create": "Willst du einen neuen Raum hinzufügen?",
|
"create": "Willst du einen neuen Raum hinzufügen?",
|
||||||
@@ -3336,7 +3361,7 @@
|
|||||||
"historical_event_no_key_backup": "Der historische Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar.",
|
"historical_event_no_key_backup": "Der historische Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar.",
|
||||||
"historical_event_unverified_device": "Sie müssen dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können",
|
"historical_event_unverified_device": "Sie müssen dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können",
|
||||||
"historical_event_user_not_joined": "Sie haben keinen Zugriff auf diese Nachricht",
|
"historical_event_user_not_joined": "Sie haben keinen Zugriff auf diese Nachricht",
|
||||||
"sender_identity_previously_verified": "Die verifizierte Identität des Absenders hat sich geändert",
|
"sender_identity_previously_verified": "Die verifizierte Identität des Absenders wurde zurückgesetzt",
|
||||||
"sender_unsigned_device": "Von einem unsicheren Gerät verschickt.",
|
"sender_unsigned_device": "Von einem unsicheren Gerät verschickt.",
|
||||||
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
|
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"reply_in_thread": "Reply in thread",
|
"reply_in_thread": "Reply in thread",
|
||||||
"report_content": "Report Content",
|
"report_content": "Report Content",
|
||||||
|
"report_room": "Report room",
|
||||||
"resend": "Resend",
|
"resend": "Resend",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
@@ -952,8 +953,8 @@
|
|||||||
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
||||||
},
|
},
|
||||||
"not_supported": "<not supported>",
|
"not_supported": "<not supported>",
|
||||||
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity appears to have changed. <a>Learn more</a>",
|
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity was reset. <a>Learn more</a>",
|
||||||
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity appears to have changed. <a>Learn more</a>",
|
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity was reset. <a>Learn more</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.",
|
"description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.",
|
||||||
"description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.",
|
"description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.",
|
||||||
@@ -1065,8 +1066,8 @@
|
|||||||
"waiting_other_user": "Waiting for %(displayName)s to verify…"
|
"waiting_other_user": "Waiting for %(displayName)s to verify…"
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Verification requested",
|
"verification_requested_toast_title": "Verification requested",
|
||||||
"verified_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) verified identity has changed. <a>Learn more</a>",
|
"verified_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity was reset. <a>Learn more</a>",
|
||||||
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>'s verified identity has changed. <a>Learn more</a>",
|
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity was reset. <a>Learn more</a>",
|
||||||
"verify_toast_description": "Other users may not trust it",
|
"verify_toast_description": "Other users may not trust it",
|
||||||
"verify_toast_title": "Verify this session",
|
"verify_toast_title": "Verify this session",
|
||||||
"withdraw_verification_action": "Withdraw verification"
|
"withdraw_verification_action": "Withdraw verification"
|
||||||
@@ -1810,6 +1811,12 @@
|
|||||||
"spam_or_propaganda": "Spam or propaganda",
|
"spam_or_propaganda": "Spam or propaganda",
|
||||||
"toxic_behaviour": "Toxic Behaviour"
|
"toxic_behaviour": "Toxic Behaviour"
|
||||||
},
|
},
|
||||||
|
"report_room": {
|
||||||
|
"description": "Report this room to your homeserver admin. This will send the room's unique ID, but if messages are encrypted, the administrator won't be able to read them or view shared files.",
|
||||||
|
"reason_placeholder": " Reason for reporting...",
|
||||||
|
"sent": "Your report was sent.",
|
||||||
|
"title": "Report Room"
|
||||||
|
},
|
||||||
"restore_key_backup_dialog": {
|
"restore_key_backup_dialog": {
|
||||||
"count_of_decryption_failures": "Failed to decrypt %(failedCount)s sessions!",
|
"count_of_decryption_failures": "Failed to decrypt %(failedCount)s sessions!",
|
||||||
"count_of_successfully_restored_keys": "Successfully restored %(sessionCount)s keys",
|
"count_of_successfully_restored_keys": "Successfully restored %(sessionCount)s keys",
|
||||||
@@ -3362,7 +3369,7 @@
|
|||||||
"historical_event_no_key_backup": "Historical messages are not available on this device",
|
"historical_event_no_key_backup": "Historical messages are not available on this device",
|
||||||
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
|
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
|
||||||
"historical_event_user_not_joined": "You don't have access to this message",
|
"historical_event_user_not_joined": "You don't have access to this message",
|
||||||
"sender_identity_previously_verified": "Sender's verified identity has changed",
|
"sender_identity_previously_verified": "Sender's verified identity was reset",
|
||||||
"sender_unsigned_device": "Sent from an insecure device.",
|
"sender_unsigned_device": "Sent from an insecure device.",
|
||||||
"unable_to_decrypt": "Unable to decrypt message"
|
"unable_to_decrypt": "Unable to decrypt message"
|
||||||
},
|
},
|
||||||
@@ -3568,7 +3575,8 @@
|
|||||||
},
|
},
|
||||||
"m.sticker": "%(senderDisplayName)s sent a sticker.",
|
"m.sticker": "%(senderDisplayName)s sent a sticker.",
|
||||||
"m.video": {
|
"m.video": {
|
||||||
"error_decrypting": "Error decrypting video"
|
"error_decrypting": "Error decrypting video",
|
||||||
|
"show_video": "Show video"
|
||||||
},
|
},
|
||||||
"m.widget": {
|
"m.widget": {
|
||||||
"added": "%(widgetName)s widget added by %(senderName)s",
|
"added": "%(widgetName)s widget added by %(senderName)s",
|
||||||
|
|||||||
@@ -1065,8 +1065,8 @@
|
|||||||
"waiting_other_user": "En attente de la vérification de %(displayName)s…"
|
"waiting_other_user": "En attente de la vérification de %(displayName)s…"
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Vérification requise",
|
"verification_requested_toast_title": "Vérification requise",
|
||||||
"verified_identity_changed": "%(displayName)s l'identité vérifiée de (<b>%(userId)s</b>) a changé. <a>En savoir plus </a>",
|
"verified_identity_changed": "L'identité de %(displayName)s (<b>%(userId)s</b>) a été réinitialisée. <a>En savoir plus</a>",
|
||||||
"verified_identity_changed_no_displayname": "L'identité vérifiée de <b>%(userId)s</b> a changé. <a>En savoir plus </a>",
|
"verified_identity_changed_no_displayname": "L'identité de <b>%(userId)s</b> a été réinitialisée. <a>En savoir plus </a>",
|
||||||
"verify_toast_description": "D’autres utilisateurs pourraient ne pas lui faire confiance",
|
"verify_toast_description": "D’autres utilisateurs pourraient ne pas lui faire confiance",
|
||||||
"verify_toast_title": "Vérifier cette session",
|
"verify_toast_title": "Vérifier cette session",
|
||||||
"withdraw_verification_action": "Révoquer la vérification"
|
"withdraw_verification_action": "Révoquer la vérification"
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"go": "Meghívás",
|
"go": "Meghívás",
|
||||||
"go_back": "Vissza",
|
"go_back": "Vissza",
|
||||||
"got_it": "Értem",
|
"got_it": "Értem",
|
||||||
|
"hide": "Elrejtés",
|
||||||
"hide_advanced": "Speciális beállítások elrejtése",
|
"hide_advanced": "Speciális beállítások elrejtése",
|
||||||
"hold": "Várakoztatás",
|
"hold": "Várakoztatás",
|
||||||
"ignore": "Mellőzés",
|
"ignore": "Mellőzés",
|
||||||
@@ -103,6 +104,7 @@
|
|||||||
"reply": "Válasz",
|
"reply": "Válasz",
|
||||||
"reply_in_thread": "Válasz üzenetszálban",
|
"reply_in_thread": "Válasz üzenetszálban",
|
||||||
"report_content": "Tartalom jelentése",
|
"report_content": "Tartalom jelentése",
|
||||||
|
"report_room": "Szoba jelentése",
|
||||||
"resend": "Újraküldés",
|
"resend": "Újraküldés",
|
||||||
"reset": "Visszaállítás",
|
"reset": "Visszaállítás",
|
||||||
"resume": "Folytatás",
|
"resume": "Folytatás",
|
||||||
@@ -407,6 +409,15 @@
|
|||||||
"download_logs": "Naplók letöltése",
|
"download_logs": "Naplók letöltése",
|
||||||
"downloading_logs": "Naplók letöltése folyamatban",
|
"downloading_logs": "Naplók letöltése folyamatban",
|
||||||
"error_empty": "Mondja el nekünk, hogy mi az, ami nem működött, vagy még jobb, ha egy GitHub-jegyben írja le a problémát.",
|
"error_empty": "Mondja el nekünk, hogy mi az, ami nem működött, vagy még jobb, ha egy GitHub-jegyben írja le a problémát.",
|
||||||
|
"failed_download_logs": "A hibakeresési naplók letöltése sikertelen: ",
|
||||||
|
"failed_send_logs_causes": {
|
||||||
|
"disallowed_app": "A hibajelentését elutasították. A rageshake kiszolgáló nem támogatja ezt az alkalmazást.",
|
||||||
|
"rejected_generic": "A hibajelentését elutasították. A rageshake kiszolgáló egy irányelv miatt elutasította a jelentés tartalmát.",
|
||||||
|
"rejected_recovery_key": "A hibajelentését biztonsági okokból elutasították, mivel helyreállítási kulcsot tartalmazott.",
|
||||||
|
"rejected_version": "A hibajelentését elutasították, mivel a futtatott verziója túl régi.",
|
||||||
|
"server_unknown_error": "A rageshake kiszolgáló ismeretlen hibát észlelt, és nem tudta kezelni a jelentést.",
|
||||||
|
"unknown_error": "A naplók elküldése sikertelen."
|
||||||
|
},
|
||||||
"github_issue": "GitHub-jegy",
|
"github_issue": "GitHub-jegy",
|
||||||
"introduction": "Ha a GitHubon keresztül küldött be hibajegyet, akkor a hibakeresési naplók segítenek nekünk felderíteni a problémát. ",
|
"introduction": "Ha a GitHubon keresztül küldött be hibajegyet, akkor a hibakeresési naplók segítenek nekünk felderíteni a problémát. ",
|
||||||
"log_request": "Segítsen abban, hogy ez később ne fordulhasson elő, <a>küldje el nekünk a naplókat</a>.",
|
"log_request": "Segítsen abban, hogy ez később ne fordulhasson elő, <a>küldje el nekünk a naplókat</a>.",
|
||||||
@@ -446,6 +457,7 @@
|
|||||||
"access_token": "Hozzáférési kulcs",
|
"access_token": "Hozzáférési kulcs",
|
||||||
"accessibility": "Akadálymentesség",
|
"accessibility": "Akadálymentesség",
|
||||||
"advanced": "Speciális",
|
"advanced": "Speciális",
|
||||||
|
"all_chats": "Összes csevegés",
|
||||||
"analytics": "Analitika",
|
"analytics": "Analitika",
|
||||||
"and_n_others": {
|
"and_n_others": {
|
||||||
"és még: %(count)s ...": "other",
|
"és még: %(count)s ...": "other",
|
||||||
@@ -1288,6 +1300,7 @@
|
|||||||
"error_connecting_heading": "Nem lehet kapcsolódni az integrációkezelőhöz",
|
"error_connecting_heading": "Nem lehet kapcsolódni az integrációkezelőhöz",
|
||||||
"explainer": "Az integrációkezelők megkapják a beállításokat, módosíthatják a kisalkalmazásokat, szobameghívókat küldhetnek és a hozzáférési szintet állíthatnak be az Ön nevében.",
|
"explainer": "Az integrációkezelők megkapják a beállításokat, módosíthatják a kisalkalmazásokat, szobameghívókat küldhetnek és a hozzáférési szintet állíthatnak be az Ön nevében.",
|
||||||
"manage_title": "Integrációk kezelése",
|
"manage_title": "Integrációk kezelése",
|
||||||
|
"toggle_label": "Az integrációkezelő engedélyezése",
|
||||||
"use_im": "Integrációkezelő használata a botok, kisalkalmazások és matricacsomagok kezeléséhez.",
|
"use_im": "Integrációkezelő használata a botok, kisalkalmazások és matricacsomagok kezeléséhez.",
|
||||||
"use_im_default": "Integrációkezelő használata <b>(%(serverName)s)</b> a botok, kisalkalmazások és matricacsomagok kezeléséhez."
|
"use_im_default": "Integrációkezelő használata <b>(%(serverName)s)</b> a botok, kisalkalmazások és matricacsomagok kezeléséhez."
|
||||||
},
|
},
|
||||||
@@ -1787,6 +1800,12 @@
|
|||||||
"spam_or_propaganda": "Kéretlen tartalom vagy propaganda",
|
"spam_or_propaganda": "Kéretlen tartalom vagy propaganda",
|
||||||
"toxic_behaviour": "Mérgező viselkedés"
|
"toxic_behaviour": "Mérgező viselkedés"
|
||||||
},
|
},
|
||||||
|
"report_room": {
|
||||||
|
"description": "A szoba jelentése a Matrix-kiszolgáló rendszergazdájának. Ez elküldi a szoba egyedi azonosítóját, de ha az üzenetek titkosítva vannak, a rendszergazda nem fogja tudni elolvasni azokat, vagy megtekinteni a megosztott fájlokat.",
|
||||||
|
"reason_placeholder": " A jelentés oka…",
|
||||||
|
"sent": "A jelentés elküldve.",
|
||||||
|
"title": "Szoba jelentése"
|
||||||
|
},
|
||||||
"restore_key_backup_dialog": {
|
"restore_key_backup_dialog": {
|
||||||
"count_of_decryption_failures": "%(failedCount)s munkamenetet nem lehet visszafejteni!",
|
"count_of_decryption_failures": "%(failedCount)s munkamenetet nem lehet visszafejteni!",
|
||||||
"count_of_successfully_restored_keys": "%(sessionCount)s kulcs sikeresen helyreállítva",
|
"count_of_successfully_restored_keys": "%(sessionCount)s kulcs sikeresen helyreállítva",
|
||||||
@@ -2069,6 +2088,19 @@
|
|||||||
"add_space_label": "Tér hozzáadása",
|
"add_space_label": "Tér hozzáadása",
|
||||||
"breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák",
|
"breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák",
|
||||||
"breadcrumbs_label": "Nemrég meglátogatott szobák",
|
"breadcrumbs_label": "Nemrég meglátogatott szobák",
|
||||||
|
"empty": {
|
||||||
|
"no_chats": "Még nincsenek csevegések",
|
||||||
|
"no_chats_description": "Kezdje azzal, hogy üzenetet küld valakinek, vagy létrehoz egy szobát",
|
||||||
|
"no_chats_description_no_room_rights": "Kezdje azzal, hogy üzenetet küld valakinek",
|
||||||
|
"no_favourites": "Még nincs kedvenc csevegése",
|
||||||
|
"no_favourites_description": "A csevegési beállításokban adhat hozzá csevegést a kedvencekhez",
|
||||||
|
"no_people": "Még nincs közvetlen csevegése senkivel",
|
||||||
|
"no_people_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez",
|
||||||
|
"no_rooms": "Még nincs egy szobában sem",
|
||||||
|
"no_rooms_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez",
|
||||||
|
"no_unread": "Gratulálunk! Nincsenek olvasatlan üzenetei.",
|
||||||
|
"show_chats": "Összes csevegés megjelenítése"
|
||||||
|
},
|
||||||
"failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s",
|
"failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s",
|
||||||
"failed_remove_tag": "Nem sikerült a szobáról eltávolítani ezt: %(tagName)s",
|
"failed_remove_tag": "Nem sikerült a szobáról eltávolítani ezt: %(tagName)s",
|
||||||
"failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani",
|
"failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani",
|
||||||
@@ -2083,10 +2115,25 @@
|
|||||||
"joining_rooms_status": {
|
"joining_rooms_status": {
|
||||||
"%(count)s szobába lép be": "other"
|
"%(count)s szobába lép be": "other"
|
||||||
},
|
},
|
||||||
|
"list_title": "Szobalista",
|
||||||
|
"more_options": {
|
||||||
|
"copy_link": "Szoba hivatkozásának másolása",
|
||||||
|
"favourited": "Kedvencnek jelölve",
|
||||||
|
"leave_room": "Szoba elhagyása",
|
||||||
|
"low_priority": "Alacsony prioritás",
|
||||||
|
"mark_read": "Megjelölés olvasottként",
|
||||||
|
"mark_unread": "Megjelölés olvasatlanként"
|
||||||
|
},
|
||||||
"notification_options": "Értesítési beállítások",
|
"notification_options": "Értesítési beállítások",
|
||||||
|
"open_space_menu": "Tér menü megnyitása",
|
||||||
|
"primary_filters": "Szobalistaszűrők",
|
||||||
"redacting_messages_status": {
|
"redacting_messages_status": {
|
||||||
"Üzenet törlése %(count)s szobából": "other"
|
"Üzenet törlése %(count)s szobából": "other"
|
||||||
},
|
},
|
||||||
|
"room": {
|
||||||
|
"more_options": "További lehetőségek",
|
||||||
|
"open_room": "A(z) %(roomName)s szoba megnyitása"
|
||||||
|
},
|
||||||
"show_less": "Kevesebb megjelenítése",
|
"show_less": "Kevesebb megjelenítése",
|
||||||
"show_n_more": {
|
"show_n_more": {
|
||||||
"Még %(count)s megjelenítése": "one"
|
"Még %(count)s megjelenítése": "one"
|
||||||
@@ -2096,6 +2143,10 @@
|
|||||||
"sort_by_activity": "Aktivitás",
|
"sort_by_activity": "Aktivitás",
|
||||||
"sort_by_alphabet": "A-Z",
|
"sort_by_alphabet": "A-Z",
|
||||||
"sort_unread_first": "Olvasatlan üzeneteket tartalmazó szobák megjelenítése elöl",
|
"sort_unread_first": "Olvasatlan üzeneteket tartalmazó szobák megjelenítése elöl",
|
||||||
|
"space_menu": {
|
||||||
|
"home": "Tér kezdőlapja",
|
||||||
|
"space_settings": "Tér beállításai"
|
||||||
|
},
|
||||||
"space_menu_label": "%(spaceName)s menű",
|
"space_menu_label": "%(spaceName)s menű",
|
||||||
"sublist_options": "Lista beállításai",
|
"sublist_options": "Lista beállításai",
|
||||||
"suggested_rooms_heading": "Javasolt szobák"
|
"suggested_rooms_heading": "Javasolt szobák"
|
||||||
@@ -2457,20 +2508,35 @@
|
|||||||
"breadcrumb_title_forgot": "Elfelejtette a helyreállítási kulcsot? Újra be kell állítania a személyazonosságát.",
|
"breadcrumb_title_forgot": "Elfelejtette a helyreállítási kulcsot? Újra be kell állítania a személyazonosságát.",
|
||||||
"breadcrumb_warning": "Csak akkor tegye ezt, ha úgy gondolja, hogy fiókját feltörték.",
|
"breadcrumb_warning": "Csak akkor tegye ezt, ha úgy gondolja, hogy fiókját feltörték.",
|
||||||
"details_title": "Titkosítás részletei",
|
"details_title": "Titkosítás részletei",
|
||||||
|
"do_not_close_warning": "Ne zárja be ezt az ablakot, amíg az alaphelyzetbe állítás be nem fejeződik",
|
||||||
"export_keys": "Kulcsok exportálása",
|
"export_keys": "Kulcsok exportálása",
|
||||||
"import_keys": "Kulcsok importálása",
|
"import_keys": "Kulcsok importálása",
|
||||||
"other_people_device_description": "Alapértelmezés szerint titkosított szobákban ne küldjön titkosított üzeneteket senkinek, amíg nem ellenőrizte őket.",
|
"other_people_device_description": "Alapértelmezés szerint titkosított szobákban ne küldjön titkosított üzeneteket senkinek, amíg nem ellenőrizte őket.",
|
||||||
"other_people_device_label": "Sose küldjön titkosított üzeneteket nem ellenőrzött eszközökre",
|
"other_people_device_label": "Sose küldjön titkosított üzeneteket nem ellenőrzött eszközökre",
|
||||||
"other_people_device_title": "Mások eszközei",
|
"other_people_device_title": "Mások eszközei",
|
||||||
"reset_identity": "Kriptográfiai személyazonosság alaphelyzetbe állítása",
|
"reset_identity": "Kriptográfiai személyazonosság alaphelyzetbe állítása",
|
||||||
|
"reset_in_progress": "Alaphelyzetbe állítás folyamatban…",
|
||||||
"session_id": "Munkamenet-azonosító:",
|
"session_id": "Munkamenet-azonosító:",
|
||||||
"session_key": "Munkamenetkulcs:",
|
"session_key": "Munkamenetkulcs:",
|
||||||
"title": "Speciális"
|
"title": "Speciális"
|
||||||
},
|
},
|
||||||
|
"delete_key_storage": {
|
||||||
|
"breadcrumb_page": "Kulcstároló törlése",
|
||||||
|
"confirm": "Kulcstároló törlése",
|
||||||
|
"description": "A kulcstároló törlése eltávolítja a kriptográfiai személyazonosságát és üzenetkulcsait a kiszolgálóról, és kikapcsolja a következő biztonsági funkciókat:",
|
||||||
|
"list_first": "Nem lesznek meg a titkosított üzenetek előzményei az új eszközein",
|
||||||
|
"list_second": "Elveszíti a titkosított üzenetekhez való hozzáférését, ha mindenhol kijelentkezik az %(brand)sből.",
|
||||||
|
"title": "Biztos, hogy kikapcsolja a kulcstárolót és törli azt?"
|
||||||
|
},
|
||||||
"device_not_verified_button": "Az eszköz ellenőrzése",
|
"device_not_verified_button": "Az eszköz ellenőrzése",
|
||||||
"device_not_verified_description": "A titkosítási beállítások megtekintéséhez ellenőriznie kell ezt az eszközt.",
|
"device_not_verified_description": "A titkosítási beállítások megtekintéséhez ellenőriznie kell ezt az eszközt.",
|
||||||
"device_not_verified_title": "Az eszköz nincs ellenőrizve",
|
"device_not_verified_title": "Az eszköz nincs ellenőrizve",
|
||||||
"dialog_title": "<strong>Beállítások:</strong> Titkosítás",
|
"dialog_title": "<strong>Beállítások:</strong> Titkosítás",
|
||||||
|
"key_storage": {
|
||||||
|
"allow_key_storage": "Kulcstárolás engedélyezése",
|
||||||
|
"description": "Tárolja biztonságosan a kriptográfiai személyazonosságát és az üzenetkulcsokat a kiszolgálón. Ez lehetővé teszi az üzenetek előzményeinek megtekintését bármely új eszközön. <a>További információk</a>",
|
||||||
|
"title": "Kulcstároló"
|
||||||
|
},
|
||||||
"recovery": {
|
"recovery": {
|
||||||
"change_recovery_confirm_button": "Új helyreállítási kulcs megerősítése",
|
"change_recovery_confirm_button": "Új helyreállítási kulcs megerősítése",
|
||||||
"change_recovery_confirm_description": "A befejezéshez adja meg alább az új helyreállítási kulcsot. A régi már nem fog működni.",
|
"change_recovery_confirm_description": "A befejezéshez adja meg alább az új helyreállítási kulcsot. A régi már nem fog működni.",
|
||||||
@@ -2587,6 +2653,7 @@
|
|||||||
"inline_url_previews_room": "Webcím-előnézetek alapértelmezett engedélyezése a szobatagok számára",
|
"inline_url_previews_room": "Webcím-előnézetek alapértelmezett engedélyezése a szobatagok számára",
|
||||||
"inline_url_previews_room_account": "Webcím-előnézetek engedélyezése ebben a szobában (csak Önt érinti)",
|
"inline_url_previews_room_account": "Webcím-előnézetek engedélyezése ebben a szobában (csak Önt érinti)",
|
||||||
"insert_trailing_colon_mentions": "Záró kettőspont beszúrása egy felhasználó üzenet elején való megemlítésekor",
|
"insert_trailing_colon_mentions": "Záró kettőspont beszúrása egy felhasználó üzenet elején való megemlítésekor",
|
||||||
|
"invite_avatars": "Azon szobák profilképének megjelenítése, melybe meghívták",
|
||||||
"jump_to_bottom_on_send": "Üzenetküldés után az idővonal aljára ugrás",
|
"jump_to_bottom_on_send": "Üzenetküldés után az idővonal aljára ugrás",
|
||||||
"key_backup": {
|
"key_backup": {
|
||||||
"backup_in_progress": "A kulcsaid mentése folyamatban van (az első mentés több percig is eltarthat).",
|
"backup_in_progress": "A kulcsaid mentése folyamatban van (az első mentés több percig is eltarthat).",
|
||||||
@@ -3064,6 +3131,7 @@
|
|||||||
"view": "Megadott címmel rendelkező szobák megjelenítése",
|
"view": "Megadott címmel rendelkező szobák megjelenítése",
|
||||||
"whois": "Információt jelenít meg a felhasználóról"
|
"whois": "Információt jelenít meg a felhasználóról"
|
||||||
},
|
},
|
||||||
|
"sliding_sync_legacy_no_longer_supported": "A régi csúszóablakos szinkronizálás már nem támogatott: jelentkezzen ki, és lépjen be újra az új csúszóablakos szinkronizálás engedélyezéséhez.",
|
||||||
"space": {
|
"space": {
|
||||||
"add_existing_room_space": {
|
"add_existing_room_space": {
|
||||||
"create": "Inkább új szobát adna hozzá?",
|
"create": "Inkább új szobát adna hozzá?",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"reply": "Svar",
|
"reply": "Svar",
|
||||||
"reply_in_thread": "Svar i tråd",
|
"reply_in_thread": "Svar i tråd",
|
||||||
"report_content": "Rapporter innhold",
|
"report_content": "Rapporter innhold",
|
||||||
|
"report_room": "Rapporter rom",
|
||||||
"resend": "Send på nytt",
|
"resend": "Send på nytt",
|
||||||
"reset": "Nullstill",
|
"reset": "Nullstill",
|
||||||
"resume": "Fortsett",
|
"resume": "Fortsett",
|
||||||
@@ -953,7 +954,7 @@
|
|||||||
},
|
},
|
||||||
"not_supported": "<not supported>",
|
"not_supported": "<not supported>",
|
||||||
"pinned_identity_changed": "%(displayName)ss (<b>%(userId)s</b>) identitet ser ut til å ha endret seg. <a>Finn ut mer</a>",
|
"pinned_identity_changed": "%(displayName)ss (<b>%(userId)s</b>) identitet ser ut til å ha endret seg. <a>Finn ut mer</a>",
|
||||||
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>s identitet ser ut til å ha endret seg. <a>Finn ut mer</a>",
|
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>s identitet ble tilbakestilt. <a>Finn ut mer</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "Denne sesjonen har oppdaget at din sikkerhetsfrase og nøkkelen for sikre meldinger har blitt fjernet.",
|
"description_1": "Denne sesjonen har oppdaget at din sikkerhetsfrase og nøkkelen for sikre meldinger har blitt fjernet.",
|
||||||
"description_2": "Hvis du gjorde dette ved et uhell, kan du konfigurere sikre meldinger på denne økten som vil kryptere denne øktens meldingshistorikk på nytt med en ny gjenopprettingsmetode.",
|
"description_2": "Hvis du gjorde dette ved et uhell, kan du konfigurere sikre meldinger på denne økten som vil kryptere denne øktens meldingshistorikk på nytt med en ny gjenopprettingsmetode.",
|
||||||
@@ -1065,8 +1066,8 @@
|
|||||||
"waiting_other_user": "Venter på at %(displayName)s skal bekrefte..."
|
"waiting_other_user": "Venter på at %(displayName)s skal bekrefte..."
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Verifisering ble forespurt",
|
"verification_requested_toast_title": "Verifisering ble forespurt",
|
||||||
"verified_identity_changed": "%(displayName)ss (<b>%(userId)s</b>) verifiserte identitet har endret seg. <a>Lær mer </a>",
|
"verified_identity_changed": "%(displayName)ss (<b>%(userId)s</b>) verifiserte identitet ble tilbakestilt. <a>Lær mer </a>",
|
||||||
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>'s verifiserte identitet har endret seg. <a>Lær mer om dette</a>",
|
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>'s verifiserte identitet ble tilbakestilt. <a>Lær mer om dette</a>",
|
||||||
"verify_toast_description": "Andre brukere kan kanskje mistro den",
|
"verify_toast_description": "Andre brukere kan kanskje mistro den",
|
||||||
"verify_toast_title": "Verifiser denne økten",
|
"verify_toast_title": "Verifiser denne økten",
|
||||||
"withdraw_verification_action": "Trekk tilbake verifisering"
|
"withdraw_verification_action": "Trekk tilbake verifisering"
|
||||||
@@ -1810,6 +1811,12 @@
|
|||||||
"spam_or_propaganda": "Spam eller propaganda",
|
"spam_or_propaganda": "Spam eller propaganda",
|
||||||
"toxic_behaviour": "Giftig oppførsel"
|
"toxic_behaviour": "Giftig oppførsel"
|
||||||
},
|
},
|
||||||
|
"report_room": {
|
||||||
|
"description": "Rapporter dette rommet til hjemmeserveradministratoren din. Dette vil sende rommets unike ID, men hvis meldinger er kryptert, vil ikke administratoren kunne lese dem eller se delte filer.",
|
||||||
|
"reason_placeholder": " Årsak til rapportering...",
|
||||||
|
"sent": "Rapporten din ble sendt.",
|
||||||
|
"title": "Rapporter rom"
|
||||||
|
},
|
||||||
"restore_key_backup_dialog": {
|
"restore_key_backup_dialog": {
|
||||||
"count_of_decryption_failures": "Kunne ikke dekryptere %(failedCount)s økter!",
|
"count_of_decryption_failures": "Kunne ikke dekryptere %(failedCount)s økter!",
|
||||||
"count_of_successfully_restored_keys": "Vellykket gjenoppretting av %(sessionCount)s nøkler",
|
"count_of_successfully_restored_keys": "Vellykket gjenoppretting av %(sessionCount)s nøkler",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"reply": "Відповісти",
|
"reply": "Відповісти",
|
||||||
"reply_in_thread": "Відповісти у гілці",
|
"reply_in_thread": "Відповісти у гілці",
|
||||||
"report_content": "Поскаржитись на вміст",
|
"report_content": "Поскаржитись на вміст",
|
||||||
|
"report_room": "Поскаржитися на кімнату",
|
||||||
"resend": "Перенадіслати",
|
"resend": "Перенадіслати",
|
||||||
"reset": "Скинути",
|
"reset": "Скинути",
|
||||||
"resume": "Продовжити",
|
"resume": "Продовжити",
|
||||||
@@ -953,8 +954,8 @@
|
|||||||
"warning": "Якщо ви не встановлювали нового способу відновлення, ймовірно хтось намагається зламати ваш обліковий запис. Негайно змініть пароль до свого облікового запису й встановіть новий спосіб відновлення в налаштуваннях."
|
"warning": "Якщо ви не встановлювали нового способу відновлення, ймовірно хтось намагається зламати ваш обліковий запис. Негайно змініть пароль до свого облікового запису й встановіть новий спосіб відновлення в налаштуваннях."
|
||||||
},
|
},
|
||||||
"not_supported": "<не підтримується>",
|
"not_supported": "<не підтримується>",
|
||||||
"pinned_identity_changed": "Схоже, особистість %(displayName)s (<b>%(userId)s</b>) змінилася. <a>Докладніше</a>",
|
"pinned_identity_changed": "Ідентичність %(displayName)s (<b>%(userId)s</b>) скинуто. <a>Докладніше</a>",
|
||||||
"pinned_identity_changed_no_displayname": "Схоже, особистість <b>%(userId)s</b> змінилася. <a>Докладніше</a>",
|
"pinned_identity_changed_no_displayname": "Ідентичність <b>%(userId)s</b> скинуто. <a>Докладніше</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "Цей сеанс виявив, що ваша фраза безпеки й ключ до захищених повідомлень були видалені.",
|
"description_1": "Цей сеанс виявив, що ваша фраза безпеки й ключ до захищених повідомлень були видалені.",
|
||||||
"description_2": "Якщо це ненароком зробили ви, налаштуйте захищені повідомлення для цього сеансу, щоб повторно зашифрувати історію листування цього сеансу з новим способом відновлення.",
|
"description_2": "Якщо це ненароком зробили ви, налаштуйте захищені повідомлення для цього сеансу, щоб повторно зашифрувати історію листування цього сеансу з новим способом відновлення.",
|
||||||
@@ -1066,8 +1067,8 @@
|
|||||||
"waiting_other_user": "Очікування звірки %(displayName)s…"
|
"waiting_other_user": "Очікування звірки %(displayName)s…"
|
||||||
},
|
},
|
||||||
"verification_requested_toast_title": "Запит перевірки",
|
"verification_requested_toast_title": "Запит перевірки",
|
||||||
"verified_identity_changed": "Підтверджена особистість %(displayName)s (<b>%(userId)s</b>) змінилася. <a>Докладніше</a>",
|
"verified_identity_changed": "Ідентичність %(displayName)s (<b>%(userId)s</b>) скинуто. <a>Докладніше</a>",
|
||||||
"verified_identity_changed_no_displayname": "Верифіковану особистість <b>%(userId)s</b> змінено. <a>Докладніше</a> ",
|
"verified_identity_changed_no_displayname": "Ідентичність <b>%(userId)s</b> скинуто. <a>Докладніше</a> ",
|
||||||
"verify_toast_description": "Інші користувачі можуть не довіряти цьому",
|
"verify_toast_description": "Інші користувачі можуть не довіряти цьому",
|
||||||
"verify_toast_title": "Звірити цей сеанс",
|
"verify_toast_title": "Звірити цей сеанс",
|
||||||
"withdraw_verification_action": "Відкликати верифікацію"
|
"withdraw_verification_action": "Відкликати верифікацію"
|
||||||
@@ -1812,6 +1813,12 @@
|
|||||||
"spam_or_propaganda": "Спам чи пропаганда",
|
"spam_or_propaganda": "Спам чи пропаганда",
|
||||||
"toxic_behaviour": "Токсична поведінка"
|
"toxic_behaviour": "Токсична поведінка"
|
||||||
},
|
},
|
||||||
|
"report_room": {
|
||||||
|
"description": "Поскаржитися на цю кімнату адміністратору домашнього сервера. Це надішле унікальний ID кімнати, але якщо повідомлення зашифровані, адміністратор не зможе прочитати їх або переглянути спільні файли.",
|
||||||
|
"reason_placeholder": "Причина для скарги...",
|
||||||
|
"sent": "Вашу скаргу надіслано.",
|
||||||
|
"title": "Поскаржитися на кімнату"
|
||||||
|
},
|
||||||
"restore_key_backup_dialog": {
|
"restore_key_backup_dialog": {
|
||||||
"count_of_decryption_failures": "Не вдалося розшифрувати %(failedCount)s сеансів!",
|
"count_of_decryption_failures": "Не вдалося розшифрувати %(failedCount)s сеансів!",
|
||||||
"count_of_successfully_restored_keys": "Успішно відновлено %(sessionCount)s ключів",
|
"count_of_successfully_restored_keys": "Успішно відновлено %(sessionCount)s ключів",
|
||||||
@@ -3366,7 +3373,7 @@
|
|||||||
"historical_event_no_key_backup": "Історичні повідомлення недоступні на цьому пристрої",
|
"historical_event_no_key_backup": "Історичні повідомлення недоступні на цьому пристрої",
|
||||||
"historical_event_unverified_device": "Щоб отримати доступ до історичних повідомлень, потрібно верифікувати цей пристрій",
|
"historical_event_unverified_device": "Щоб отримати доступ до історичних повідомлень, потрібно верифікувати цей пристрій",
|
||||||
"historical_event_user_not_joined": "Ви не маєте доступу до цього повідомлення",
|
"historical_event_user_not_joined": "Ви не маєте доступу до цього повідомлення",
|
||||||
"sender_identity_previously_verified": "Верифіковану особистість змінено",
|
"sender_identity_previously_verified": "Верифіковану ідентичність відправника скинуто",
|
||||||
"sender_unsigned_device": "Зашифровано пристроєм, який не верифіковано його власником.",
|
"sender_unsigned_device": "Зашифровано пристроєм, який не верифіковано його власником.",
|
||||||
"unable_to_decrypt": "Не вдалося розшифрувати повідомлення"
|
"unable_to_decrypt": "Не вдалося розшифрувати повідомлення"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface INotificationStateSnapshotParams {
|
|||||||
level: NotificationLevel;
|
level: NotificationLevel;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
knocked: boolean;
|
knocked: boolean;
|
||||||
|
invited: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationStateEvents {
|
export enum NotificationStateEvents {
|
||||||
@@ -38,6 +39,7 @@ export abstract class NotificationState
|
|||||||
protected _level: NotificationLevel = NotificationLevel.None;
|
protected _level: NotificationLevel = NotificationLevel.None;
|
||||||
protected _muted = false;
|
protected _muted = false;
|
||||||
protected _knocked = false;
|
protected _knocked = false;
|
||||||
|
protected _invited = false;
|
||||||
|
|
||||||
private watcherReferences: string[] = [];
|
private watcherReferences: string[] = [];
|
||||||
|
|
||||||
@@ -70,10 +72,22 @@ export abstract class NotificationState
|
|||||||
return this._knocked;
|
return this._knocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is an invitation notification.
|
||||||
|
* Invite notifications are a special case of highlight notifications
|
||||||
|
*/
|
||||||
|
public get invited(): boolean {
|
||||||
|
return this._invited;
|
||||||
|
}
|
||||||
|
|
||||||
public get isIdle(): boolean {
|
public get isIdle(): boolean {
|
||||||
return this.level <= NotificationLevel.None;
|
return this.level <= NotificationLevel.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is higher than an activity notification or if the feature_hidebold is disabled with an activity notification.
|
||||||
|
* The "unread" term used here is different from the "Unread" in the UI. Unread in the UI doesn't include activity notifications even with feature_hidebold disabled.
|
||||||
|
*/
|
||||||
public get isUnread(): boolean {
|
public get isUnread(): boolean {
|
||||||
if (this.level > NotificationLevel.Activity) {
|
if (this.level > NotificationLevel.Activity) {
|
||||||
return true;
|
return true;
|
||||||
@@ -83,10 +97,19 @@ export abstract class NotificationState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification has a count or a symbol and is equal or greater than an NotificationLevel.Notification.
|
||||||
|
*/
|
||||||
public get hasUnreadCount(): boolean {
|
public get hasUnreadCount(): boolean {
|
||||||
return this.level >= NotificationLevel.Notification && (!!this.count || !!this.symbol);
|
return this.level >= NotificationLevel.Notification && (!!this.count || !!this.symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is a mention, an invitation, a knock or a unset message.
|
||||||
|
*
|
||||||
|
* @deprecated because the name is confusing. A mention is not an invitation, a knock or an unsent message.
|
||||||
|
* In case of a {@link RoomNotificationState}, use {@link RoomNotificationState.isMention} instead.
|
||||||
|
*/
|
||||||
public get hasMentions(): boolean {
|
public get hasMentions(): boolean {
|
||||||
return this.level >= NotificationLevel.Highlight;
|
return this.level >= NotificationLevel.Highlight;
|
||||||
}
|
}
|
||||||
@@ -116,6 +139,7 @@ export class NotificationStateSnapshot {
|
|||||||
private readonly level: NotificationLevel;
|
private readonly level: NotificationLevel;
|
||||||
private readonly muted: boolean;
|
private readonly muted: boolean;
|
||||||
private readonly knocked: boolean;
|
private readonly knocked: boolean;
|
||||||
|
private readonly isInvitation: boolean;
|
||||||
|
|
||||||
public constructor(state: INotificationStateSnapshotParams) {
|
public constructor(state: INotificationStateSnapshotParams) {
|
||||||
this.symbol = state.symbol;
|
this.symbol = state.symbol;
|
||||||
@@ -123,6 +147,7 @@ export class NotificationStateSnapshot {
|
|||||||
this.level = state.level;
|
this.level = state.level;
|
||||||
this.muted = state.muted;
|
this.muted = state.muted;
|
||||||
this.knocked = state.knocked;
|
this.knocked = state.knocked;
|
||||||
|
this.isInvitation = state.invited;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
|
public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
|
||||||
@@ -132,6 +157,7 @@ export class NotificationStateSnapshot {
|
|||||||
level: this.level,
|
level: this.level,
|
||||||
muted: this.muted,
|
muted: this.muted,
|
||||||
knocked: this.knocked,
|
knocked: this.knocked,
|
||||||
|
is: this.isInvitation,
|
||||||
};
|
};
|
||||||
const after = {
|
const after = {
|
||||||
count: other.count,
|
count: other.count,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import * as RoomNotifs from "../../RoomNotifs";
|
|||||||
import { NotificationState } from "./NotificationState";
|
import { NotificationState } from "./NotificationState";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications";
|
import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications";
|
||||||
|
import { NotificationLevel } from "./NotificationLevel";
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
public constructor(
|
public constructor(
|
||||||
@@ -51,6 +52,52 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||||||
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is a mention.
|
||||||
|
*/
|
||||||
|
public get isMention(): boolean {
|
||||||
|
if (this.invited || this.knocked) return false;
|
||||||
|
|
||||||
|
return this.level === NotificationLevel.Highlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is an unset message.
|
||||||
|
*/
|
||||||
|
public get isUnsetMessage(): boolean {
|
||||||
|
return this.level === NotificationLevel.Unsent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity notifications are the lowest level of notification (except none and muted)
|
||||||
|
*/
|
||||||
|
public get isActivityNotification(): boolean {
|
||||||
|
return this.level === NotificationLevel.Activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the case for notifications with a level:
|
||||||
|
* - is a knock
|
||||||
|
* - greater Activity
|
||||||
|
* - equal Activity and feature_hidebold is disabled.
|
||||||
|
*/
|
||||||
|
public get hasAnyNotificationOrActivity(): boolean {
|
||||||
|
if (this.knocked) return true;
|
||||||
|
|
||||||
|
// If the feature_hidebold is enabled, we don't want to show activity notifications
|
||||||
|
const hideBold = SettingsStore.getValue("feature_hidebold");
|
||||||
|
if (!hideBold && this.level === NotificationLevel.Activity) return true;
|
||||||
|
|
||||||
|
return this.level >= NotificationLevel.Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the notification is a NotificationLevel.Notification.
|
||||||
|
*/
|
||||||
|
public get isNotification(): boolean {
|
||||||
|
return this.level === NotificationLevel.Notification;
|
||||||
|
}
|
||||||
|
|
||||||
private handleLocalEchoUpdated = (): void => {
|
private handleLocalEchoUpdated = (): void => {
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
@@ -95,7 +142,11 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||||||
private updateNotificationState(): void {
|
private updateNotificationState(): void {
|
||||||
const snapshot = this.snapshot();
|
const snapshot = this.snapshot();
|
||||||
|
|
||||||
const { level, symbol, count } = RoomNotifs.determineUnreadState(this.room, undefined, this.includeThreads);
|
const { level, symbol, count, invited } = RoomNotifs.determineUnreadState(
|
||||||
|
this.room,
|
||||||
|
undefined,
|
||||||
|
this.includeThreads,
|
||||||
|
);
|
||||||
const muted =
|
const muted =
|
||||||
RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute;
|
RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute;
|
||||||
const knocked =
|
const knocked =
|
||||||
@@ -105,6 +156,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||||||
this._count = count;
|
this._count = count;
|
||||||
this._muted = muted;
|
this._muted = muted;
|
||||||
this._knocked = knocked;
|
this._knocked = knocked;
|
||||||
|
this._invited = invited;
|
||||||
|
|
||||||
// finally, publish an update if needed
|
// finally, publish an update if needed
|
||||||
this.emitIfUpdated(snapshot);
|
this.emitIfUpdated(snapshot);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { MentionsFilter } from "./skip-list/filters/MentionsFilter";
|
|||||||
import { LowPriorityFilter } from "./skip-list/filters/LowPriorityFilter";
|
import { LowPriorityFilter } from "./skip-list/filters/LowPriorityFilter";
|
||||||
import { type Sorter, SortingAlgorithm } from "./skip-list/sorters";
|
import { type Sorter, SortingAlgorithm } from "./skip-list/sorters";
|
||||||
import { SettingLevel } from "../../settings/SettingLevel";
|
import { SettingLevel } from "../../settings/SettingLevel";
|
||||||
|
import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are the filters passed to the room skip list.
|
* These are the filters passed to the room skip list.
|
||||||
@@ -156,6 +157,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "MatrixActions.Room.accountData": {
|
||||||
|
const eventType = payload.event_type;
|
||||||
|
if (eventType === MARKED_UNREAD_TYPE_STABLE || eventType === MARKED_UNREAD_TYPE_UNSTABLE) {
|
||||||
|
const room = payload.room;
|
||||||
|
this.addRoomAndEmit(room);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "MatrixActions.Event.decrypted": {
|
case "MatrixActions.Event.decrypted": {
|
||||||
const roomId = payload.event.getRoomId();
|
const roomId = payload.event.getRoomId();
|
||||||
if (!roomId) return;
|
if (!roomId) return;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import type { Room } from "matrix-js-sdk/src/matrix";
|
|||||||
import type { Filter } from ".";
|
import type { Filter } from ".";
|
||||||
import { FilterKey } from ".";
|
import { FilterKey } from ".";
|
||||||
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";
|
||||||
|
import { getMarkedUnreadState } from "../../../../utils/notifications";
|
||||||
|
|
||||||
export class UnreadFilter implements Filter {
|
export class UnreadFilter implements Filter {
|
||||||
public matches(room: Room): boolean {
|
public matches(room: Room): boolean {
|
||||||
return RoomNotificationStateStore.instance.getRoomState(room).hasUnreadCount;
|
return RoomNotificationStateStore.instance.getRoomState(room).hasUnreadCount || !!getMarkedUnreadState(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get key(): FilterKey.UnreadFilter {
|
public get key(): FilterKey.UnreadFilter {
|
||||||
|
|||||||
@@ -113,4 +113,18 @@ export class MediaEventHelper implements IDestroyable {
|
|||||||
// Finally, it's probably not media
|
// Finally, it's probably not media
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the media event in question supports being hidden in the timeline.
|
||||||
|
* @param event Any matrix event.
|
||||||
|
* @returns `true` if the media can be hidden, otherwise false.
|
||||||
|
*/
|
||||||
|
public static canHide(event: MatrixEvent): boolean {
|
||||||
|
if (!event) return false;
|
||||||
|
if (event.isRedacted()) return false;
|
||||||
|
const content = event.getContent();
|
||||||
|
const hideTypes: string[] = [MsgType.Video, MsgType.Image];
|
||||||
|
if (hideTypes.includes(content.msgtype!)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const SESSION_LOCK_CONSTANTS = {
|
|||||||
/**
|
/**
|
||||||
* The number of milliseconds after which we consider a lock claim stale
|
* The number of milliseconds after which we consider a lock claim stale
|
||||||
*/
|
*/
|
||||||
LOCK_EXPIRY_TIME_MS: 30000,
|
LOCK_EXPIRY_TIME_MS: 15000,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,7 +140,10 @@ export async function getSessionLock(onNewInstance: () => Promise<void>): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeAgo = Date.now() - parseInt(lastPingTime);
|
const timeAgo = Date.now() - parseInt(lastPingTime);
|
||||||
const remaining = SESSION_LOCK_CONSTANTS.LOCK_EXPIRY_TIME_MS - timeAgo;
|
// If the last ping time is in the future (i.e., timeAgo is negative), the chances are that the system clock has
|
||||||
|
// been wound back since the ping. Rather than waiting hours/days/millenia for us to get there, treat a future
|
||||||
|
// ping as "just now" by clipping to 0.
|
||||||
|
const remaining = SESSION_LOCK_CONSTANTS.LOCK_EXPIRY_TIME_MS - Math.max(timeAgo, 0);
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
// another session claimed the lock, but it is stale.
|
// another session claimed the lock, but it is stale.
|
||||||
prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago: proceeding with startup`);
|
prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago: proceeding with startup`);
|
||||||
@@ -242,13 +245,23 @@ export async function getSessionLock(onNewInstance: () => Promise<void>): Promis
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We construct our own promise here rather than using the `sleep` utility, to make it easier to test the
|
||||||
|
// SessionLock in a separate Window.
|
||||||
const sleepPromise = new Promise((resolve) => {
|
const sleepPromise = new Promise((resolve) => {
|
||||||
setTimeout(resolve, remaining, undefined);
|
setTimeout(resolve, remaining, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("storage", onStorageUpdate!);
|
window.addEventListener("storage", onStorageUpdate!);
|
||||||
await Promise.race([sleepPromise, storageUpdatePromise]);
|
const winner = await Promise.race([sleepPromise, storageUpdatePromise]);
|
||||||
window.removeEventListener("storage", onStorageUpdate!);
|
window.removeEventListener("storage", onStorageUpdate!);
|
||||||
|
|
||||||
|
// If we got through the whole of the sleep without any writes to the store, we know that the
|
||||||
|
// ping is now stale. There's no point in going round and calling `checkLock` again: we know that
|
||||||
|
// nothing has changed since last time.
|
||||||
|
if (!(winner instanceof StorageEvent)) {
|
||||||
|
prefixedLogger.info("Existing claim went stale: proceeding with startup");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, we know the lock is ours for the taking.
|
// If we get here, we know the lock is ours for the taking.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { mocked, type MockedObject } from "jest-mock";
|
|||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
import StorageEvictedDialog from "../../src/components/views/dialogs/StorageEvictedDialog";
|
import StorageEvictedDialog from "../../src/components/views/dialogs/StorageEvictedDialog";
|
||||||
import { logout, restoreSessionFromStorage, setLoggedIn } from "../../src/Lifecycle";
|
import * as Lifecycle from "../../src/Lifecycle";
|
||||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||||
import Modal from "../../src/Modal";
|
import Modal from "../../src/Modal";
|
||||||
import * as StorageAccess from "../../src/utils/StorageAccess";
|
import * as StorageAccess from "../../src/utils/StorageAccess";
|
||||||
@@ -28,19 +28,25 @@ import { Action } from "../../src/dispatcher/actions";
|
|||||||
import PlatformPeg from "../../src/PlatformPeg";
|
import PlatformPeg from "../../src/PlatformPeg";
|
||||||
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../../src/utils/tokens/tokens";
|
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../../src/utils/tokens/tokens";
|
||||||
import { encryptPickleKey } from "../../src/utils/tokens/pickling";
|
import { encryptPickleKey } from "../../src/utils/tokens/pickling";
|
||||||
|
import * as StorageManager from "../../src/utils/StorageManager.ts";
|
||||||
|
import type BasePlatform from "../../src/BasePlatform.ts";
|
||||||
|
|
||||||
|
const { logout, restoreSessionFromStorage, setLoggedIn } = Lifecycle;
|
||||||
|
|
||||||
const webCrypto = new Crypto();
|
const webCrypto = new Crypto();
|
||||||
|
|
||||||
const windowCrypto = window.crypto;
|
const windowCrypto = window.crypto;
|
||||||
|
|
||||||
describe("Lifecycle", () => {
|
describe("Lifecycle", () => {
|
||||||
const mockPlatform = mockPlatformPeg();
|
let mockPlatform: MockedObject<BasePlatform>;
|
||||||
|
|
||||||
const realLocalStorage = global.localStorage;
|
const realLocalStorage = global.localStorage;
|
||||||
|
|
||||||
let mockClient!: MockedObject<MatrixJs.MatrixClient>;
|
let mockClient!: MockedObject<MatrixJs.MatrixClient>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
mockPlatform = mockPlatformPeg();
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(),
|
...mockClientMethodsUser(),
|
||||||
stopClient: jest.fn(),
|
stopClient: jest.fn(),
|
||||||
@@ -182,6 +188,32 @@ describe("Lifecycle", () => {
|
|||||||
mac: expect.any(String),
|
mac: expect.any(String),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe("loadSession", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// stub this out
|
||||||
|
jest.spyOn(Modal, "createDialog").mockReturnValue(
|
||||||
|
// @ts-ignore allow bad mock
|
||||||
|
{ finished: Promise.resolve([true]) },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show any error dialog when checkConsistency throws but abortSignal has triggered", async () => {
|
||||||
|
jest.spyOn(StorageManager, "checkConsistency").mockRejectedValue(new Error("test error"));
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const prom = Lifecycle.loadSession({
|
||||||
|
enableGuest: true,
|
||||||
|
guestHsUrl: "https://guest.server",
|
||||||
|
fragmentQueryParams: { guest_user_id: "a", guest_access_token: "b" },
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
});
|
||||||
|
abortController.abort();
|
||||||
|
await expect(prom).resolves.toBeFalsy();
|
||||||
|
|
||||||
|
expect(Modal.createDialog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("restoreSessionFromStorage()", () => {
|
describe("restoreSessionFromStorage()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initLocalStorageMock();
|
initLocalStorageMock();
|
||||||
|
|||||||
@@ -314,34 +314,51 @@ describe("RoomListViewModel", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("active index", () => {
|
describe("Sticky room and active index", () => {
|
||||||
it("should recalculate active index when list of rooms change", () => {
|
function expectActiveRoom(vm: ReturnType<typeof useRoomListViewModel>, i: number, roomId: string) {
|
||||||
|
expect(vm.activeIndex).toEqual(i);
|
||||||
|
expect(vm.rooms[i].roomId).toEqual(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("active room and active index are retained on order change", () => {
|
||||||
const { rooms } = mockAndCreateRooms();
|
const { rooms } = mockAndCreateRooms();
|
||||||
// Let's say that the first room is the active room initially
|
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => rooms[0].roomId);
|
// Let's say that the room at index 5 is active
|
||||||
|
const roomId = rooms[5].roomId;
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||||
|
|
||||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
expect(vm.current.activeIndex).toEqual(0);
|
expect(vm.current.activeIndex).toEqual(5);
|
||||||
|
|
||||||
// Let's say that a new room is added and that becomes active
|
// Let's say that room at index 9 moves to index 5
|
||||||
const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined);
|
const room9 = rooms[9];
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => newRoom.roomId);
|
rooms.splice(9, 1);
|
||||||
rooms.push(newRoom);
|
rooms.splice(5, 0, room9);
|
||||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
|
||||||
// Now the active room should be the last room which we just added
|
// Active room index should still be 5
|
||||||
expect(vm.current.activeIndex).toEqual(rooms.length - 1);
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
|
||||||
|
// Let's add 2 new rooms from index 0
|
||||||
|
const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined);
|
||||||
|
const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined);
|
||||||
|
rooms.unshift(newRoom1, newRoom2);
|
||||||
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
|
||||||
|
// Active room index should still be 5
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should recalculate active index when active room changes", () => {
|
it("active room and active index are updated when another room is opened", () => {
|
||||||
const { rooms } = mockAndCreateRooms();
|
const { rooms } = mockAndCreateRooms();
|
||||||
|
const roomId = rooms[5].roomId;
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||||
|
|
||||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
|
||||||
// No active room yet
|
// Let's say that room at index 9 becomes active
|
||||||
expect(vm.current.activeIndex).toBeUndefined();
|
const room = rooms[9];
|
||||||
|
|
||||||
// Let's say that room at index 5 becomes active
|
|
||||||
const room = rooms[5];
|
|
||||||
act(() => {
|
act(() => {
|
||||||
dispatcher.dispatch(
|
dispatcher.dispatch(
|
||||||
{
|
{
|
||||||
@@ -353,8 +370,76 @@ describe("RoomListViewModel", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We expect index 5 to be active now
|
// Active room index should change to reflect new room
|
||||||
expect(vm.current.activeIndex).toEqual(5);
|
expectActiveRoom(vm.current, 9, room.roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("active room and active index are updated when active index spills out of rooms array bounds", () => {
|
||||||
|
const { rooms } = mockAndCreateRooms();
|
||||||
|
// Let's say that the room at index 5 is active
|
||||||
|
const roomId = rooms[5].roomId;
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||||
|
|
||||||
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
|
||||||
|
// Let's say that we remove rooms from the start of the array
|
||||||
|
for (let i = 0; i < 4; ++i) {
|
||||||
|
// We should be able to do 4 deletions before we run out of rooms
|
||||||
|
rooms.splice(0, 1);
|
||||||
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we remove one more room from the start, there's not going to be enough rooms
|
||||||
|
// to maintain the active index.
|
||||||
|
rooms.splice(0, 1);
|
||||||
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
expectActiveRoom(vm.current, 0, roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("active room and active index are retained when rooms that appear after the active room are deleted", () => {
|
||||||
|
const { rooms } = mockAndCreateRooms();
|
||||||
|
// Let's say that the room at index 5 is active
|
||||||
|
const roomId = rooms[5].roomId;
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||||
|
|
||||||
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
|
||||||
|
// Let's say that we remove rooms from the start of the array
|
||||||
|
for (let i = 0; i < 4; ++i) {
|
||||||
|
// Deleting rooms after index 5 (active) should not update the active index
|
||||||
|
rooms.splice(6, 1);
|
||||||
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("active room index becomes undefined when active room is deleted", () => {
|
||||||
|
const { rooms } = mockAndCreateRooms();
|
||||||
|
// Let's say that the room at index 5 is active
|
||||||
|
let roomId: string | undefined = rooms[5].roomId;
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||||
|
|
||||||
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
|
expectActiveRoom(vm.current, 5, roomId);
|
||||||
|
|
||||||
|
// Let's remove the active room (i.e room at index 5)
|
||||||
|
rooms.splice(5, 1);
|
||||||
|
roomId = undefined;
|
||||||
|
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||||
|
expect(vm.current.activeIndex).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("active room index is initially undefined", () => {
|
||||||
|
mockAndCreateRooms();
|
||||||
|
|
||||||
|
// Let's say that there's no active room currently
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
|
expect(vm.current.activeIndex).toEqual(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
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 { render } from "jest-matrix-react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ReportRoomDialog } from "../../../../../src/components/views/dialogs/ReportRoomDialog";
|
||||||
|
import SdkConfig from "../../../../../src/SdkConfig";
|
||||||
|
import { stubClient } from "../../../../test-utils";
|
||||||
|
|
||||||
|
const ROOM_ID = "!foo:bar";
|
||||||
|
|
||||||
|
describe("ReportRoomDialog", () => {
|
||||||
|
const onFinished: jest.Mock<any, any> = jest.fn();
|
||||||
|
const reportRoom: jest.Mock<any, any> = jest.fn();
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
const client = stubClient();
|
||||||
|
client.reportRoom = reportRoom;
|
||||||
|
|
||||||
|
SdkConfig.put({
|
||||||
|
report_event: {
|
||||||
|
admin_message_md: `
|
||||||
|
# You should know
|
||||||
|
|
||||||
|
This doesn't actually go **anywhere**.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
SdkConfig.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can close the dialog", async () => {
|
||||||
|
const { getByTestId } = render(<ReportRoomDialog roomId={ROOM_ID} onFinished={onFinished} />);
|
||||||
|
await userEvent.click(getByTestId("dialog-cancel-button"));
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays admin message", async () => {
|
||||||
|
const { container } = render(<ReportRoomDialog roomId={ROOM_ID} onFinished={onFinished} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can submit a report", async () => {
|
||||||
|
const REASON = "This room is bad!";
|
||||||
|
const { getByLabelText, getByText, getByRole } = render(
|
||||||
|
<ReportRoomDialog roomId={ROOM_ID} onFinished={onFinished} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.type(getByLabelText("Reason"), REASON);
|
||||||
|
await userEvent.click(getByRole("button", { name: "Send report" }));
|
||||||
|
|
||||||
|
expect(reportRoom).toHaveBeenCalledWith(ROOM_ID, REASON);
|
||||||
|
expect(getByText("Your report was sent.")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(getByRole("button", { name: "Close dialog" }));
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ReportRoomDialog displays admin message 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-describedby="mx_ReportEventDialog"
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_ReportRoomDialog mx_Dialog_fixedWidth"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Report Room
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
class="_root_19upo_16"
|
||||||
|
id="mx_ReportEventDialog"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Report this room to your homeserver admin. This will send the room's unique ID, but if messages are encrypted, the administrator won't be able to read them or view shared files.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<h1>
|
||||||
|
You should know
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This doesn't actually go
|
||||||
|
<strong>
|
||||||
|
anywhere
|
||||||
|
</strong>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="_field_19upo_26"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="_label_19upo_59"
|
||||||
|
for="mx_ReportRoomDialog_reason"
|
||||||
|
>
|
||||||
|
Reason
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="mx_ReportRoomDialog_reason"
|
||||||
|
placeholder=" Reason for reporting..."
|
||||||
|
rows="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Send report
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
aria-label="Close dialog"
|
||||||
|
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { act } from "react";
|
import React, { act } from "react";
|
||||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
import encrypt from "matrix-encrypt-attachment";
|
import encrypt from "matrix-encrypt-attachment";
|
||||||
@@ -85,6 +85,10 @@ describe("<MImageBody/>", () => {
|
|||||||
fetchMock.mockReset();
|
fetchMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mocked(encrypt.decryptAttachment).mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
it("should show a thumbnail while image is being downloaded", async () => {
|
it("should show a thumbnail while image is being downloaded", async () => {
|
||||||
fetchMock.getOnce(url, { status: 200 });
|
fetchMock.getOnce(url, { status: 200 });
|
||||||
|
|
||||||
@@ -166,6 +170,8 @@ describe("<MImageBody/>", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Show image")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(fetchMock).not.toHaveFetched(url);
|
expect(fetchMock).not.toHaveFetched(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,8 +192,12 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
expect(fetchMock).toHaveFetched(url);
|
expect(fetchMock).toHaveFetched(url);
|
||||||
|
|
||||||
// spinner while downloading image
|
// Show image is asynchronous since it applies through a settings watcher hook, so
|
||||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
// be sure to wait here.
|
||||||
|
await waitFor(() => {
|
||||||
|
// spinner while downloading image
|
||||||
|
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { act } from "react";
|
||||||
import { EventType, getHttpUriForMxc, type IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { EventType, getHttpUriForMxc, type IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { render, type RenderResult } from "jest-matrix-react";
|
import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||||
@@ -22,19 +22,22 @@ import {
|
|||||||
mockClientMethodsUser,
|
mockClientMethodsUser,
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
||||||
|
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
|
||||||
|
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||||
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
|
||||||
|
// Needed so we don't throw an error about failing to decrypt.
|
||||||
|
jest.mock("matrix-encrypt-attachment", () => ({
|
||||||
|
decryptAttachment: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("MVideoBody", () => {
|
describe("MVideoBody", () => {
|
||||||
it("does not crash when given a portrait image", () => {
|
const userId = "@user:server";
|
||||||
// Check for an unreliable crash caused by a fractional-sized
|
const deviceId = "DEADB33F";
|
||||||
// image dimension being used for a CanvasImageData.
|
|
||||||
const { asFragment } = makeMVideoBody(720, 1280);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
// If we get here, we did not crash.
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show poster for encrypted media before downloading it", async () => {
|
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||||
const userId = "@user:server";
|
|
||||||
const deviceId = "DEADB33F";
|
beforeEach(() => {
|
||||||
const cli = getMockClientWithEventEmitter({
|
const cli = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(userId),
|
...mockClientMethodsUser(userId),
|
||||||
...mockClientMethodsServer(),
|
...mockClientMethodsServer(),
|
||||||
@@ -49,40 +52,108 @@ describe("MVideoBody", () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
|
||||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
// eslint-disable-next-line no-restricted-properties
|
||||||
cli.mxcUrlToHttp.mockImplementation(
|
cli.mxcUrlToHttp.mockImplementation(
|
||||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const encryptedMediaEvent = new MatrixEvent({
|
fetchMock.mockReset();
|
||||||
room_id: "!room:server",
|
});
|
||||||
sender: userId,
|
|
||||||
type: EventType.RoomMessage,
|
const encryptedMediaEvent = new MatrixEvent({
|
||||||
content: {
|
room_id: "!room:server",
|
||||||
body: "alt for a test video",
|
sender: userId,
|
||||||
info: {
|
type: EventType.RoomMessage,
|
||||||
duration: 420,
|
content: {
|
||||||
w: 40,
|
body: "alt for a test video",
|
||||||
h: 50,
|
info: {
|
||||||
thumbnail_file: {
|
duration: 420,
|
||||||
url: "mxc://server/encrypted-poster",
|
w: 40,
|
||||||
},
|
h: 50,
|
||||||
},
|
thumbnail_file: {
|
||||||
file: {
|
url: "mxc://server/encrypted-poster",
|
||||||
url: "mxc://server/encrypted-image",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
file: {
|
||||||
|
url: "mxc://server/encrypted-image",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not crash when given a portrait image", () => {
|
||||||
|
// Check for an unreliable crash caused by a fractional-sized
|
||||||
|
// image dimension being used for a CanvasImageData.
|
||||||
|
const { asFragment } = makeMVideoBody(720, 1280);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
// If we get here, we did not crash.
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show poster for encrypted media before downloading it", async () => {
|
||||||
|
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("with video previews/thumbnails disabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
act(() => {
|
||||||
|
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => {
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showImages",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showImages"),
|
||||||
|
);
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showMediaEventIds",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showMediaEventIds"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not download video", async () => {
|
||||||
|
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MVideoBody
|
||||||
|
mxEvent={encryptedMediaEvent}
|
||||||
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Show video")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(fetchMock).not.toHaveFetched(thumbUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render video poster after user consent", async () => {
|
||||||
|
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MVideoBody
|
||||||
|
mxEvent={encryptedMediaEvent}
|
||||||
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderButton = screen.getByRole("button", { name: "Show video" });
|
||||||
|
|
||||||
|
expect(placeholderButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(placeholderButton);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveFetched(thumbUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeMVideoBody(w: number, h: number): RenderResult {
|
function makeMVideoBody(w: number, h: number): RenderResult {
|
||||||
@@ -109,7 +180,7 @@ function makeMVideoBody(w: number, h: number): RenderResult {
|
|||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultProps: MVideoBody["props"] = {
|
const defaultProps: IBodyProps = {
|
||||||
mxEvent: event,
|
mxEvent: event,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
highlightLink: "",
|
highlightLink: "",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ exports[`DecryptionFailureBody should handle messages from users who change iden
|
|||||||
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
|
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Sender's verified identity has changed
|
Sender's verified identity was reset
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ exports[`<MImageBody/> should generate a thumbnail if one isn't included for ani
|
|||||||
GIF
|
GIF
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style="height: 50px; width: 40px;"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,9 +74,6 @@ exports[`<MImageBody/> should show a thumbnail while image is being downloaded 1
|
|||||||
<div
|
<div
|
||||||
style="max-height: 50px; max-width: 40px;"
|
style="max-height: 50px; max-width: 40px;"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
style="height: 50px; width: 40px;"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -609,46 +609,87 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
|||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
<div
|
||||||
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
class="mx_RoomSummaryCard_bottomOptions"
|
||||||
data-kind="critical"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<svg
|
<button
|
||||||
aria-hidden="true"
|
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
class="_icon_1x5l4_34"
|
data-kind="critical"
|
||||||
fill="currentColor"
|
role="menuitem"
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
aria-hidden="true"
|
||||||
/>
|
class="_icon_1x5l4_34"
|
||||||
<path
|
fill="currentColor"
|
||||||
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
height="24"
|
||||||
/>
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
width="24"
|
||||||
<span
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
>
|
||||||
|
<path
|
||||||
|
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
|
>
|
||||||
|
Leave room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
|
data-kind="critical"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
Leave room
|
<svg
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<svg
|
class="_icon_1x5l4_34"
|
||||||
aria-hidden="true"
|
fill="currentColor"
|
||||||
class="_nav-hint_1x5l4_50"
|
height="24"
|
||||||
fill="currentColor"
|
viewBox="0 0 24 24"
|
||||||
height="24"
|
width="24"
|
||||||
viewBox="8 0 8 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="8"
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path
|
||||||
>
|
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||||
<path
|
/>
|
||||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
</svg>
|
||||||
/>
|
<span
|
||||||
</svg>
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
</button>
|
>
|
||||||
|
Report room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1227,46 +1268,87 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
|||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
<div
|
||||||
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
class="mx_RoomSummaryCard_bottomOptions"
|
||||||
data-kind="critical"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<svg
|
<button
|
||||||
aria-hidden="true"
|
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
class="_icon_1x5l4_34"
|
data-kind="critical"
|
||||||
fill="currentColor"
|
role="menuitem"
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
aria-hidden="true"
|
||||||
/>
|
class="_icon_1x5l4_34"
|
||||||
<path
|
fill="currentColor"
|
||||||
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
height="24"
|
||||||
/>
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
width="24"
|
||||||
<span
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
>
|
||||||
|
<path
|
||||||
|
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
|
>
|
||||||
|
Leave room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
|
data-kind="critical"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
Leave room
|
<svg
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<svg
|
class="_icon_1x5l4_34"
|
||||||
aria-hidden="true"
|
fill="currentColor"
|
||||||
class="_nav-hint_1x5l4_50"
|
height="24"
|
||||||
fill="currentColor"
|
viewBox="0 0 24 24"
|
||||||
height="24"
|
width="24"
|
||||||
viewBox="8 0 8 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="8"
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path
|
||||||
>
|
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||||
<path
|
/>
|
||||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
</svg>
|
||||||
/>
|
<span
|
||||||
</svg>
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
</button>
|
>
|
||||||
|
Report room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1882,46 +1964,87 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
|||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
<div
|
||||||
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
class="mx_RoomSummaryCard_bottomOptions"
|
||||||
data-kind="critical"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<svg
|
<button
|
||||||
aria-hidden="true"
|
class="mx_RoomSummaryCard_leave _item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
class="_icon_1x5l4_34"
|
data-kind="critical"
|
||||||
fill="currentColor"
|
role="menuitem"
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
aria-hidden="true"
|
||||||
/>
|
class="_icon_1x5l4_34"
|
||||||
<path
|
fill="currentColor"
|
||||||
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
height="24"
|
||||||
/>
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
width="24"
|
||||||
<span
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
>
|
||||||
|
<path
|
||||||
|
d="M14 13q.424 0 .713-.287A.97.97 0 0 0 15 12a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 12q0 .424.287.713.288.287.713.287"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.385 21.788A1 1 0 0 1 10 21V3a1.003 1.003 0 0 1 1.242-.97l8 2A1 1 0 0 1 20 5v14a1 1 0 0 1-.758.97l-8 2a1 1 0 0 1-.857-.182M18 5.781l-6-1.5v15.438l6-1.5zM9 6H7v12h2v2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
|
>
|
||||||
|
Leave room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_item_1x5l4_8 _interactive_1x5l4_26"
|
||||||
|
data-kind="critical"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
Leave room
|
<svg
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<svg
|
class="_icon_1x5l4_34"
|
||||||
aria-hidden="true"
|
fill="currentColor"
|
||||||
class="_nav-hint_1x5l4_50"
|
height="24"
|
||||||
fill="currentColor"
|
viewBox="0 0 24 24"
|
||||||
height="24"
|
width="24"
|
||||||
viewBox="8 0 8 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="8"
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path
|
||||||
>
|
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||||
<path
|
/>
|
||||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
</svg>
|
||||||
/>
|
<span
|
||||||
</svg>
|
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_1x5l4_43"
|
||||||
</button>
|
>
|
||||||
|
Report room
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1x5l4_50"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ describe("EventTile", () => {
|
|||||||
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
|
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
|
||||||
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
|
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
|
||||||
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
|
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
|
||||||
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity has changed"],
|
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity was reset"],
|
||||||
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
|
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
|
||||||
mxEvent = await mkEncryptedMatrixEvent({
|
mxEvent = await mkEncryptedMatrixEvent({
|
||||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
|||||||
@@ -145,9 +145,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
);
|
);
|
||||||
await userEvent.click(screen.getByRole("button")!);
|
await userEvent.click(screen.getByRole("button")!);
|
||||||
await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org"));
|
await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org"));
|
||||||
@@ -168,7 +166,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(getWarningByText("Alice's (@alice:example.org) verified identity has changed.")).toBeInTheDocument(),
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -192,8 +190,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
await sleep(10); // give it some time to finish initialising
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
|
||||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity was reset.")).toThrow();
|
||||||
expect(() => getWarningByText("Alice's (@alice:example.org) verified identity has changed.")).toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// We don't display warnings in non-encrypted rooms, but if encryption is
|
// We don't display warnings in non-encrypted rooms, but if encryption is
|
||||||
@@ -213,7 +210,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
|
|
||||||
await sleep(10); // give it some time to finish initialising
|
await sleep(10); // give it some time to finish initialising
|
||||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity was reset.")).toThrow();
|
||||||
|
|
||||||
// Encryption gets enabled in the room. We should now warn that Alice's
|
// Encryption gets enabled in the room. We should now warn that Alice's
|
||||||
// identity changed.
|
// identity changed.
|
||||||
@@ -234,9 +231,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,9 +250,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
crypto.pinCurrentUserIdentity = jest.fn();
|
crypto.pinCurrentUserIdentity = jest.fn();
|
||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getWarningByText("@a:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
expect(getWarningByText("@a:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Ensure existing prompt stays even if a new violation with lower lexicographic order detected", async () => {
|
it("Ensure existing prompt stays even if a new violation with lower lexicographic order detected", async () => {
|
||||||
@@ -273,9 +266,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
crypto.pinCurrentUserIdentity = jest.fn();
|
crypto.pinCurrentUserIdentity = jest.fn();
|
||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getWarningByText("@b:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
expect(getWarningByText("@b:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate a new member joined with lower lexico order and also in violation
|
// Simulate a new member joined with lower lexico order and also in violation
|
||||||
mockMembershipForRoom(room, ["@a:example.org", "@b:example.org"]);
|
mockMembershipForRoom(room, ["@a:example.org", "@b:example.org"]);
|
||||||
@@ -285,9 +276,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// We should still display the warning for @b:example.org
|
// We should still display the warning for @b:example.org
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getWarningByText("@b:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
expect(getWarningByText("@b:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +296,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
await sleep(50);
|
await sleep(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity was reset.")).toThrow();
|
||||||
|
|
||||||
// The user changes their identity, so we should show the warning.
|
// The user changes their identity, so we should show the warning.
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -317,9 +306,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate the user's new identity having been approved, so we no
|
// Simulate the user's new identity having been approved, so we no
|
||||||
@@ -330,7 +317,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
client.emit(CryptoEvent.UserTrustStatusChanged, "@alice:example.org", newStatus);
|
client.emit(CryptoEvent.UserTrustStatusChanged, "@alice:example.org", newStatus);
|
||||||
});
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(),
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity was reset.")).toThrow(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -355,7 +342,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
expect(getWarningByText("@alice:example.org's identity was reset.")).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bob is invited. His identity needs approval, so we should show a
|
// Bob is invited. His identity needs approval, so we should show a
|
||||||
@@ -372,12 +359,8 @@ describe("UserIdentityWarning", () => {
|
|||||||
emitMembershipChange(client, "@alice:example.org", "leave");
|
emitMembershipChange(client, "@alice:example.org", "leave");
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(() => getWarningByText("@alice:example.org's identity was reset.")).toThrow());
|
||||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
await waitFor(() => expect(getWarningByText("@bob:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
);
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when invited users cannot see encrypted messages", async () => {
|
it("when invited users cannot see encrypted messages", async () => {
|
||||||
@@ -399,7 +382,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
emitMembershipChange(client, "@alice:example.org", "join");
|
emitMembershipChange(client, "@alice:example.org", "join");
|
||||||
});
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
expect(getWarningByText("@alice:example.org's identity was reset.")).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bob is invited. His identity needs approval, but we don't encrypt
|
// Bob is invited. His identity needs approval, but we don't encrypt
|
||||||
@@ -419,12 +402,8 @@ describe("UserIdentityWarning", () => {
|
|||||||
mockMembershipForRoom(room, [["@bob:example.org", "invited"]]);
|
mockMembershipForRoom(room, [["@bob:example.org", "invited"]]);
|
||||||
emitMembershipChange(client, "@alice:example.org", "leave");
|
emitMembershipChange(client, "@alice:example.org", "leave");
|
||||||
});
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(() => getWarningByText("@alice:example.org's identity was reset.")).toThrow());
|
||||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
await waitFor(() => expect(() => getWarningByText("@bob:example.org's identity was reset.")).toThrow());
|
||||||
);
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(() => getWarningByText("@bob:example.org's identity appears to have changed.")).toThrow(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when member leaves immediately after component is loaded", async () => {
|
it("when member leaves immediately after component is loaded", async () => {
|
||||||
@@ -448,7 +427,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
await sleep(10);
|
await sleep(10);
|
||||||
});
|
});
|
||||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
expect(() => getWarningByText("@alice:example.org's identity was reset.")).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when member leaves immediately after joining", async () => {
|
it("when member leaves immediately after joining", async () => {
|
||||||
@@ -496,7 +475,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
await sleep(10); // give it some time to finish
|
await sleep(10); // give it some time to finish
|
||||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
expect(() => getWarningByText("@alice:example.org's identity was reset.")).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -516,9 +495,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
// We should warn about Alice's identity first.
|
// We should warn about Alice's identity first.
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate Alice's new identity having been approved, so now we warn
|
// Simulate Alice's new identity having been approved, so now we warn
|
||||||
@@ -534,9 +511,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
});
|
});
|
||||||
client.emit(CryptoEvent.UserTrustStatusChanged, "@alice:example.org", newStatus);
|
client.emit(CryptoEvent.UserTrustStatusChanged, "@alice:example.org", newStatus);
|
||||||
});
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getWarningByText("@bob:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays the next user when the verification requirement is withdrawn", async () => {
|
it("displays the next user when the verification requirement is withdrawn", async () => {
|
||||||
@@ -556,7 +531,7 @@ describe("UserIdentityWarning", () => {
|
|||||||
renderComponent(client, room);
|
renderComponent(client, room);
|
||||||
// We should warn about Alice's identity first.
|
// We should warn about Alice's identity first.
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(getWarningByText("Alice's (@alice:example.org) verified identity has changed.")).toBeInTheDocument(),
|
expect(getWarningByText("Alice's (@alice:example.org) identity was reset.")).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate Alice's new identity having been approved, so now we warn
|
// Simulate Alice's new identity having been approved, so now we warn
|
||||||
@@ -575,8 +550,6 @@ describe("UserIdentityWarning", () => {
|
|||||||
new UserVerificationStatus(false, false, false, false),
|
new UserVerificationStatus(false, false, false, false),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getWarningByText("@bob:example.org's identity was reset.")).toBeInTheDocument());
|
||||||
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe("<AdvancedPanel />", () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId("otherSettings")).toMatchSnapshot();
|
expect(screen.getByTestId("otherSettings")).toMatchSnapshot();
|
||||||
const checkbox = screen.getByRole("checkbox", {
|
const checkbox = screen.getByRole("checkbox", {
|
||||||
name: "Never send encrypted messages to unverified devices",
|
name: "In encrypted rooms, only send messages to verified users",
|
||||||
});
|
});
|
||||||
expect(checkbox).toBeChecked();
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ exports[`<AdvancedPanel /> <OtherSettings /> should display the blacklist of unv
|
|||||||
class="_label_19upo_59"
|
class="_label_19upo_59"
|
||||||
for="radix-:r6:"
|
for="radix-:r6:"
|
||||||
>
|
>
|
||||||
Never send encrypted messages to unverified devices
|
In encrypted rooms, only send messages to verified users
|
||||||
</label>
|
</label>
|
||||||
<span
|
<span
|
||||||
class="_message_19upo_85 _help-message_19upo_91"
|
class="_message_19upo_85 _help-message_19upo_91"
|
||||||
|
|||||||
@@ -392,13 +392,13 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Never send encrypted messages to unverified sessions from this session
|
Only send messages to verified users
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="false"
|
aria-checked="false"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-label="Never send encrypted messages to unverified sessions from this session"
|
aria-label="Only send messages to verified users"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||||
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||||
role="switch"
|
role="switch"
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import { RoomNotificationState } from "../../../../src/stores/notifications/Room
|
|||||||
import { NotificationStateEvents } from "../../../../src/stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../../src/stores/notifications/NotificationState";
|
||||||
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
|
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
|
||||||
import { createMessageEventContent } from "../../../test-utils/events";
|
import { createMessageEventContent } from "../../../test-utils/events";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import * as RoomStatusBarModule from "../../../../src/components/structures/RoomStatusBar";
|
||||||
|
import * as UnreadModule from "../../../../src/Unread";
|
||||||
|
|
||||||
describe("RoomNotificationState", () => {
|
describe("RoomNotificationState", () => {
|
||||||
let room: Room;
|
let room: Room;
|
||||||
@@ -36,6 +39,10 @@ describe("RoomNotificationState", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
function addThread(room: Room): void {
|
function addThread(room: Room): void {
|
||||||
const threadId = "thread_id";
|
const threadId = "thread_id";
|
||||||
jest.spyOn(room, "eventShouldLiveIn").mockReturnValue({
|
jest.spyOn(room, "eventShouldLiveIn").mockReturnValue({
|
||||||
@@ -200,4 +207,85 @@ describe("RoomNotificationState", () => {
|
|||||||
expect(roomNotifState.level).toBe(NotificationLevel.Activity);
|
expect(roomNotifState.level).toBe(NotificationLevel.Activity);
|
||||||
expect(roomNotifState.symbol).toBe(null);
|
expect(roomNotifState.symbol).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("computed attributes", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([]);
|
||||||
|
jest.spyOn(UnreadModule, "doesRoomHaveUnreadMessages").mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has invited at true", () => {
|
||||||
|
room.updateMyMembership(KnownMembership.Invite);
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.invited).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has isUnsetMessage at true", () => {
|
||||||
|
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.isUnsetMessage).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has isMention at false if the notification is invitation, an unset message or a knock", () => {
|
||||||
|
setUnreads(room, 0, 2);
|
||||||
|
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.isMention).toBe(true);
|
||||||
|
|
||||||
|
room.updateMyMembership(KnownMembership.Invite);
|
||||||
|
expect(roomNotifState.isMention).toBe(false);
|
||||||
|
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
room.updateMyMembership(KnownMembership.Knock);
|
||||||
|
expect(roomNotifState.isMention).toBe(false);
|
||||||
|
|
||||||
|
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||||
|
room.updateMyMembership(KnownMembership.Join);
|
||||||
|
expect(roomNotifState.isMention).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has isNotification at true", () => {
|
||||||
|
setUnreads(room, 1, 0);
|
||||||
|
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.isNotification).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has isActivityNotification at true", () => {
|
||||||
|
jest.spyOn(UnreadModule, "doesRoomHaveUnreadMessages").mockReturnValue(true);
|
||||||
|
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.isActivityNotification).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should has hasAnyNotificationOrActivity at true", () => {
|
||||||
|
// Hidebold is disabled
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
|
// Unread message, generate activity notification
|
||||||
|
jest.spyOn(UnreadModule, "doesRoomHaveUnreadMessages").mockReturnValue(true);
|
||||||
|
// Highlight notification
|
||||||
|
setUnreads(room, 0, 1);
|
||||||
|
|
||||||
|
// There is one highlight notification
|
||||||
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
|
expect(roomNotifState.hasAnyNotificationOrActivity).toBe(true);
|
||||||
|
|
||||||
|
// Activity notification
|
||||||
|
setUnreads(room, 0, 0);
|
||||||
|
// Trigger update
|
||||||
|
room.updateMyMembership(KnownMembership.Join);
|
||||||
|
// hidebold is disabled and we have an activity notification
|
||||||
|
expect(roomNotifState.hasAnyNotificationOrActivity).toBe(true);
|
||||||
|
|
||||||
|
// hidebold is enabled and we have an activity notification
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
room.updateMyMembership(KnownMembership.Join);
|
||||||
|
expect(roomNotifState.hasAnyNotificationOrActivity).toBe(false);
|
||||||
|
|
||||||
|
// No unread
|
||||||
|
jest.spyOn(UnreadModule, "doesRoomHaveUnreadMessages").mockReturnValue(false);
|
||||||
|
room.updateMyMembership(KnownMembership.Join);
|
||||||
|
expect(roomNotifState.hasAnyNotificationOrActivity).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { RoomNotificationStateStore } from "../../../../src/stores/notifications
|
|||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
|
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import * as utils from "../../../../src/utils/notifications";
|
||||||
|
|
||||||
describe("RoomListStoreV3", () => {
|
describe("RoomListStoreV3", () => {
|
||||||
async function getRoomListStore() {
|
async function getRoomListStore() {
|
||||||
@@ -474,6 +475,36 @@ describe("RoomListStoreV3", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("unread filter matches rooms that are marked as unread", async () => {
|
||||||
|
const { client, rooms } = getClientAndRooms();
|
||||||
|
// Let's choose 5 rooms to put in space
|
||||||
|
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
|
||||||
|
|
||||||
|
setupMocks(spaceRoom, roomIds);
|
||||||
|
const store = new RoomListStoreV3Class(dispatcher);
|
||||||
|
await store.start();
|
||||||
|
|
||||||
|
// Since there's no unread yet, we expect zero results
|
||||||
|
let result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]);
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
|
||||||
|
// Mock so that room at index 8 is marked as unread
|
||||||
|
jest.spyOn(utils, "getMarkedUnreadState").mockImplementation((room) => room.roomId === rooms[8].roomId);
|
||||||
|
dispatcher.dispatch(
|
||||||
|
{
|
||||||
|
action: "MatrixActions.Room.accountData",
|
||||||
|
room: rooms[8],
|
||||||
|
event_type: utils.MARKED_UNREAD_TYPE_STABLE,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now we expect room at index 8 to show as unread
|
||||||
|
result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result).toContain(rooms[8]);
|
||||||
|
});
|
||||||
|
|
||||||
it("supports filtering by people and rooms", async () => {
|
it("supports filtering by people and rooms", async () => {
|
||||||
const { client, rooms } = getClientAndRooms();
|
const { client, rooms } = getClientAndRooms();
|
||||||
// Let's choose 5 rooms to put in space
|
// Let's choose 5 rooms to put in space
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ describe("ImportanceAlgorithm", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unreadStates: Record<string, ReturnType<(typeof RoomNotifs)["determineUnreadState"]>> = {
|
const unreadStates: Record<string, ReturnType<(typeof RoomNotifs)["determineUnreadState"]>> = {
|
||||||
red: { symbol: null, count: 1, level: NotificationLevel.Highlight },
|
red: { symbol: null, count: 1, level: NotificationLevel.Highlight, invited: false },
|
||||||
grey: { symbol: null, count: 1, level: NotificationLevel.Notification },
|
grey: { symbol: null, count: 1, level: NotificationLevel.Notification, invited: false },
|
||||||
none: { symbol: null, count: 0, level: NotificationLevel.None },
|
none: { symbol: null, count: 0, level: NotificationLevel.None, invited: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -77,6 +77,7 @@ describe("ImportanceAlgorithm", () => {
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
level: NotificationLevel.None,
|
level: NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,6 +184,7 @@ describe("ImportanceAlgorithm", () => {
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
level: NotificationLevel.None,
|
level: NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
});
|
});
|
||||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||||
|
|
||||||
@@ -353,6 +355,7 @@ describe("ImportanceAlgorithm", () => {
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
level: NotificationLevel.None,
|
level: NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
});
|
});
|
||||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ describe("NaturalAlgorithm", () => {
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
level: NotificationLevel.None,
|
level: NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,15 +71,15 @@ describe("SessionLock", () => {
|
|||||||
jest.advanceTimersByTime(5000);
|
jest.advanceTimersByTime(5000);
|
||||||
expect(checkSessionLockFree()).toBe(false);
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
// second instance tries to start. This should block for 25 more seconds
|
// second instance tries to start. This should block for 10 more seconds
|
||||||
const onNewInstance2 = jest.fn();
|
const onNewInstance2 = jest.fn();
|
||||||
let session2Result: boolean | undefined;
|
let session2Result: boolean | undefined;
|
||||||
getSessionLock(onNewInstance2).then((res) => {
|
getSessionLock(onNewInstance2).then((res) => {
|
||||||
session2Result = res;
|
session2Result = res;
|
||||||
});
|
});
|
||||||
|
|
||||||
// after another 24.5 seconds, we are still waiting
|
// after another 9.5 seconds, we are still waiting
|
||||||
jest.advanceTimersByTime(24500);
|
jest.advanceTimersByTime(9500);
|
||||||
expect(session2Result).toBe(undefined);
|
expect(session2Result).toBe(undefined);
|
||||||
expect(checkSessionLockFree()).toBe(false);
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
@@ -92,6 +92,40 @@ describe("SessionLock", () => {
|
|||||||
expect(onNewInstance2).not.toHaveBeenCalled();
|
expect(onNewInstance2).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("A second instance starts up when the first terminated uncleanly and the clock was wound back", async () => {
|
||||||
|
// first instance starts...
|
||||||
|
expect(await getSessionLock(() => Promise.resolve())).toBe(true);
|
||||||
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
|
// oops, now it dies. We simulate this by forcibly clearing the timers.
|
||||||
|
const time = Date.now();
|
||||||
|
jest.clearAllTimers();
|
||||||
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
|
// Now, the clock gets wound back an hour.
|
||||||
|
jest.setSystemTime(time - 3600 * 1000);
|
||||||
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
|
// second instance tries to start. This should block for 15 seconds
|
||||||
|
const onNewInstance2 = jest.fn();
|
||||||
|
let session2Result: boolean | undefined;
|
||||||
|
getSessionLock(onNewInstance2).then((res) => {
|
||||||
|
session2Result = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
// after another 14.5 seconds, we are still waiting
|
||||||
|
jest.advanceTimersByTime(14500);
|
||||||
|
expect(session2Result).toBe(undefined);
|
||||||
|
expect(checkSessionLockFree()).toBe(false);
|
||||||
|
|
||||||
|
// another 500ms and we get the lock
|
||||||
|
await jest.advanceTimersByTimeAsync(500);
|
||||||
|
expect(session2Result).toBe(true);
|
||||||
|
expect(checkSessionLockFree()).toBe(false); // still false, because the new session has claimed it
|
||||||
|
|
||||||
|
expect(onNewInstance2).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("A second instance waits for the first to shut down", async () => {
|
it("A second instance waits for the first to shut down", async () => {
|
||||||
// first instance starts. Once it gets the shutdown signal, it will wait two seconds and then release the lock.
|
// first instance starts. Once it gets the shutdown signal, it will wait two seconds and then release the lock.
|
||||||
await getSessionLock(
|
await getSessionLock(
|
||||||
|
|||||||
Reference in New Issue
Block a user