From df7bb61a5b24aeb9ac6acb30d599a5507dda788e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Sep 2024 14:42:27 +0100 Subject: [PATCH 01/40] Experimental SSS Working branch to get SSS functional on element-web. Requires https://github.com/matrix-org/matrix-js-sdk/pull/4400 --- src/SlidingSyncManager.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 578a1757490..2752e2ce1c1 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -214,9 +214,9 @@ export class SlidingSyncManager { try { // if we only have range changes then call a different function so we don't nuke the list from before if (updateArgs.ranges && Object.keys(updateArgs).length === 1) { - await this.slidingSync!.setListRanges(listKey, updateArgs.ranges); + this.slidingSync!.setListRanges(listKey, updateArgs.ranges); } else { - await this.slidingSync!.setList(listKey, list); + this.slidingSync!.setList(listKey, list); } } catch (err) { logger.debug("ensureListRegistered: update failed txn_id=", err); @@ -266,21 +266,14 @@ export class SlidingSyncManager { */ public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - let startIndex = batchSize; + let windowSize = batchSize; let hasMore = true; let firstTime = true; while (hasMore) { - const endIndex = startIndex + batchSize - 1; try { - const ranges = [ - [0, batchSize - 1], - [startIndex, endIndex], - ]; if (firstTime) { await this.slidingSync!.setList(SlidingSyncManager.ListSearch, { - // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure - // any changes to the list whilst spidering are caught. - ranges: ranges, + ranges: [[0, windowSize]], sort: [ "by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough ], @@ -313,8 +306,8 @@ export class SlidingSyncManager { await sleep(gapBetweenRequestsMs); } const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!; - hasMore = endIndex + 1 < listData.joinedCount; - startIndex += batchSize; + hasMore = windowSize < listData.joinedCount; + windowSize += batchSize; firstTime = false; } } @@ -375,7 +368,7 @@ export class SlidingSyncManager { public async nativeSlidingSyncSupport(client: MatrixClient): Promise { // Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561 // `client` can be undefined/null in tests for some reason. - const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575"); + const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575"); if (support) { logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable"); } From 7fbac104902ed6f8a50e4cd5b51b4db8051fb70a Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:45:40 +0100 Subject: [PATCH 02/40] Adjust tests to use new behaviour --- src/SlidingSyncManager.ts | 10 +++---- test/SlidingSyncManager-test.ts | 52 +++++++-------------------------- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 2752e2ce1c1..97ee976f60e 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -266,14 +266,14 @@ export class SlidingSyncManager { */ public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - let windowSize = batchSize; + let fetchUpTo = batchSize; let hasMore = true; let firstTime = true; while (hasMore) { try { if (firstTime) { await this.slidingSync!.setList(SlidingSyncManager.ListSearch, { - ranges: [[0, windowSize]], + ranges: [[0, fetchUpTo]], sort: [ "by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough ], @@ -296,7 +296,7 @@ export class SlidingSyncManager { }, }); } else { - await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges); + await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, [[0,fetchUpTo]]); } } catch (err) { // do nothing, as we reject only when we get interrupted but that's fine as the next @@ -306,8 +306,8 @@ export class SlidingSyncManager { await sleep(gapBetweenRequestsMs); } const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!; - hasMore = windowSize < listData.joinedCount; - windowSize += batchSize; + hasMore = fetchUpTo < listData.joinedCount; + fetchUpTo += batchSize; firstTime = false; } } diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 3625bf2a18e..343269c947e 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -79,7 +79,6 @@ describe("SlidingSyncManager", () => { it("creates a new list based on the key", async () => { const listKey = "key"; mocked(slidingSync.getListParams).mockReturnValue(null); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { sort: ["by_recency"], }); @@ -114,7 +113,6 @@ describe("SlidingSyncManager", () => { mocked(slidingSync.getListParams).mockReturnValue({ ranges: [[0, 42]], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { ranges: [[0, 52]], }); @@ -128,7 +126,6 @@ describe("SlidingSyncManager", () => { ranges: [[0, 42]], sort: ["by_recency"], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { ranges: [[0, 42]], sort: ["by_recency"], @@ -139,11 +136,9 @@ describe("SlidingSyncManager", () => { }); describe("startSpidering", () => { - it("requests in batchSizes", async () => { + it("requests in expanding batchSizes", async () => { const gapMs = 1; const batchSize = 10; - mocked(slidingSync.setList).mockResolvedValue("yep"); - mocked(slidingSync.setListRanges).mockResolvedValue("yep"); mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 64, @@ -153,12 +148,13 @@ describe("SlidingSyncManager", () => { await manager.startSpidering(batchSize, gapMs); // we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69 const wantWindows = [ - [10, 19], - [20, 29], - [30, 39], - [40, 49], - [50, 59], - [60, 69], + [0, 10], + [0, 20], + [0, 30], + [0, 40], + [0, 50], + [0, 60], + [0, 70], ]; expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length); expect(slidingSync.setList).toHaveBeenCalledTimes(1); @@ -170,13 +166,12 @@ describe("SlidingSyncManager", () => { SlidingSyncManager.ListSearch, // eslint-disable-next-line jest/no-conditional-expect expect.objectContaining({ - ranges: [[0, batchSize - 1], range], + ranges: [range], }), ); return; } expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [ - [0, batchSize - 1], range, ]); }); @@ -184,30 +179,6 @@ describe("SlidingSyncManager", () => { it("handles accounts with zero rooms", async () => { const gapMs = 1; const batchSize = 10; - mocked(slidingSync.setList).mockResolvedValue("yep"); - mocked(slidingSync.getListData).mockImplementation((key) => { - return { - joinedCount: 0, - roomIndexToRoomId: {}, - }; - }); - await manager.startSpidering(batchSize, gapMs); - expect(slidingSync.getListData).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - expect.objectContaining({ - ranges: [ - [0, batchSize - 1], - [batchSize, batchSize + batchSize - 1], - ], - }), - ); - }); - it("continues even when setList rejects", async () => { - const gapMs = 1; - const batchSize = 10; - mocked(slidingSync.setList).mockRejectedValue("narp"); mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 0, @@ -221,8 +192,7 @@ describe("SlidingSyncManager", () => { SlidingSyncManager.ListSearch, expect.objectContaining({ ranges: [ - [0, batchSize - 1], - [batchSize, batchSize + batchSize - 1], + [0, batchSize], ], }), ); @@ -277,7 +247,7 @@ describe("SlidingSyncManager", () => { const unstableSpy = jest .spyOn(client, "doesServerSupportUnstableFeature") .mockImplementation(async (feature: string) => { - expect(feature).toBe("org.matrix.msc3575"); + expect(feature).toBe("org.matrix.simplified_msc3575"); return true; }); const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); From 194362a4879b6e7ea1b6ed33e19e0f66945dfb18 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:33:29 +0100 Subject: [PATCH 03/40] Remove well-known proxy URL lookup; always use native This is actually required for SSS because otherwise it would use the proxy over native support. --- src/SlidingSyncManager.ts | 51 ++--------------------- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 5 --- test/SlidingSyncManager-test.ts | 73 +-------------------------------- 4 files changed, 6 insertions(+), 125 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 97ee976f60e..bdee8e202f7 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -322,44 +322,12 @@ export class SlidingSyncManager { * @returns A working Sliding Sync or undefined */ public async setup(client: MatrixClient): Promise { - const baseUrl = client.baseUrl; - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const wellKnownProxyUrl = await this.getProxyFromWellKnown(client); - - const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl; - - this.configure(client, slidingSyncEndpoint); - logger.info("Sliding sync activated at", slidingSyncEndpoint); + this.configure(client, client.baseUrl); + logger.info("Simplified Sliding Sync activated at", client.baseUrl); this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart - return this.slidingSync; } - /** - * Get the sliding sync proxy URL from the client well known - * @param client The MatrixClient to use - * @return The proxy url - */ - public async getProxyFromWellKnown(client: MatrixClient): Promise { - let proxyUrl: string | undefined; - - try { - const clientDomain = await client.getDomain(); - if (clientDomain === null) { - throw new RangeError("Homeserver domain is null"); - } - const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain); - proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; - } catch (e) { - // Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown - } - - if (proxyUrl != undefined) { - logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); - } - return proxyUrl; - } - /** * Check if the server "natively" supports sliding sync (with an unstable endpoint). * @param client The MatrixClient to use @@ -370,7 +338,7 @@ export class SlidingSyncManager { // `client` can be undefined/null in tests for some reason. const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575"); if (support) { - logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable"); + logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable"); } return support; } @@ -387,17 +355,6 @@ export class SlidingSyncManager { SlidingSyncController.serverSupportsSlidingSync = true; return; } - - const proxyUrl = await this.getProxyFromWellKnown(client); - if (proxyUrl != undefined) { - const response = await fetch(new URL("/client/server.json", proxyUrl), { - method: Method.Get, - signal: timeoutSignal(10 * 1000), // 10s - }); - if (response.status === 200) { - logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); - SlidingSyncController.serverSupportsSlidingSync = true; - } - } + SlidingSyncController.serverSupportsSlidingSync = false; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3130de7a764..24de78a6cc4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1470,7 +1470,7 @@ "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", - "sliding_sync": "Sliding Sync mode", + "sliding_sync": "Simplified Sliding Sync mode", "sliding_sync_description": "Under active development, cannot be disabled.", "sliding_sync_disabled_notice": "Log out and back in to disable", "sliding_sync_server_no_support": "Your server lacks support", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5e5c9a15355..bf9ae417e25 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -388,11 +388,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { default: false, controller: new SlidingSyncController(), }, - "feature_sliding_sync_proxy_url": { - // This is not a distinct feature, it is a legacy setting for feature_sliding_sync above - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: "", - }, "feature_element_call_video_rooms": { isFeature: true, labsGroup: LabGroup.VoiceAndVideo, diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 343269c947e..4fdac937426 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -14,7 +14,6 @@ import fetchMockJest from "fetch-mock-jest"; import { SlidingSyncManager } from "../src/SlidingSyncManager"; import { stubClient } from "./test-utils"; import SlidingSyncController from "../src/settings/controllers/SlidingSyncController"; -import SettingsStore from "../src/settings/SettingsStore"; jest.mock("matrix-js-sdk/src/sliding-sync"); const MockSlidingSync = >(SlidingSync); @@ -41,7 +40,6 @@ describe("SlidingSyncManager", () => { const roomId = "!room:id"; const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep"); await manager.setRoomVisible(roomId, true); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); }); @@ -67,7 +65,6 @@ describe("SlidingSyncManager", () => { }); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep"); await manager.setRoomVisible(roomId, true); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); // we aren't prescriptive about what the sub name is. @@ -95,7 +92,6 @@ describe("SlidingSyncManager", () => { mocked(slidingSync.getListParams).mockReturnValue({ ranges: [[0, 42]], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { sort: ["by_recency"], }); @@ -201,61 +197,11 @@ describe("SlidingSyncManager", () => { describe("checkSupport", () => { beforeEach(() => { SlidingSyncController.serverSupportsSlidingSync = false; - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); }); it("shorts out if the server has 'native' sliding sync support", async () => { jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => { - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).toHaveBeenCalled(); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - it("should query well-known on server_name not baseUrl", async () => { - fetchMockJest.get("https://matrix.org/.well-known/matrix/client", { - "m.homeserver": { - base_url: "https://matrix-client.matrix.org", - server: "matrix.org", - }, - "org.matrix.msc3575.proxy": { - url: "https://proxy/", - }, - }); - fetchMockJest.get("https://matrix-client.matrix.org/_matrix/client/versions", { versions: ["v1.4"] }); - - mocked(manager.getProxyFromWellKnown).mockRestore(); - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - expect(fetchMockJest).not.toHaveFetched("https://matrix-client.matrix.org/.well-known/matrix/client"); - }); - }); - describe("nativeSlidingSyncSupport", () => { - beforeEach(() => { - SlidingSyncController.serverSupportsSlidingSync = false; - }); - it("should make an OPTIONS request to avoid unintended side effects", async () => { - // See https://github.com/element-hq/element-web/issues/27426 - - const unstableSpy = jest - .spyOn(client, "doesServerSupportUnstableFeature") - .mockImplementation(async (feature: string) => { - expect(feature).toBe("org.matrix.simplified_msc3575"); - return true; - }); - const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); // first thing it does is call nativeSlidingSyncSupport - expect(proxySpy).not.toHaveBeenCalled(); - expect(unstableSpy).toHaveBeenCalled(); expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); }); }); @@ -264,28 +210,11 @@ describe("SlidingSyncManager", () => { jest.spyOn(manager, "configure"); jest.spyOn(manager, "startSpidering"); }); - it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => { + it("uses the baseUrl", async () => { await manager.setup(client); expect(manager.configure).toHaveBeenCalled(); expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl); expect(manager.startSpidering).toHaveBeenCalled(); }); - it("uses the proxy declared in the client well-known", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "https://proxy/"); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { - if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy"; - }); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy"); - expect(manager.startSpidering).toHaveBeenCalled(); - }); }); }); From 48eec705ec324af1d51dd3503e6c2871b109839d Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:54:02 +0100 Subject: [PATCH 04/40] Linting --- src/SlidingSyncManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index bdee8e202f7..6f663167695 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -36,7 +36,7 @@ Please see LICENSE files in the repository root for full details. * list ops) */ -import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix"; import { MSC3575Filter, MSC3575List, @@ -48,7 +48,6 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; -import SettingsStore from "./settings/SettingsStore"; import SlidingSyncController from "./settings/controllers/SlidingSyncController"; // how long to long poll for From 8325c5934dc3befbe3f0537601f606dabfc952ae Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:34:14 +0100 Subject: [PATCH 05/40] Debug logging --- src/components/structures/TimelinePanel.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index f4c9b639acb..1d71b28c550 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -319,6 +319,7 @@ class TimelinePanel extends React.Component { } public componentDidMount(): void { + console.log("componentDidMount"); if (this.props.manageReadReceipts) { this.updateReadReceiptOnUserActivity(); } @@ -364,6 +365,7 @@ class TimelinePanel extends React.Component { } public componentWillUnmount(): void { + console.log("componentWillUnmount"); // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -704,6 +706,7 @@ class TimelinePanel extends React.Component { removed: boolean, data: IRoomTimelineData, ): void => { + console.log("onRoomTimeline ", ev, room, "toStart:",toStartOfTimeline, "removed:",removed,"data:",data); // ignore events for other timeline sets if ( data.timeline.getTimelineSet() !== this.props.timelineSet && @@ -711,7 +714,6 @@ class TimelinePanel extends React.Component { ) { return; } - if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) { if (toStartOfTimeline && !this.state.canBackPaginate) { this.setState({ @@ -810,6 +812,7 @@ class TimelinePanel extends React.Component { } private onRoomTimelineReset = (room: Room | undefined, timelineSet: EventTimelineSet): void => { + console.log("onRoomTimelineReset", room, timelineSet); if (timelineSet !== this.props.timelineSet && timelineSet !== this.props.overlayTimelineSet) return; if (this.canResetTimeline()) { @@ -1425,6 +1428,7 @@ class TimelinePanel extends React.Component { }; private initTimeline(props: IProps): void { + console.log("initTimeline"); const initialEvent = props.eventId; const pixelOffset = props.eventPixelOffset; @@ -1534,6 +1538,7 @@ class TimelinePanel extends React.Component { * @param {boolean?} scrollIntoView whether to scroll the event into view. */ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void { + console.log("loadTimeline"); const cli = MatrixClientPeg.safeGet(); this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap }); this.overlayTimelineWindow = this.props.overlayTimelineSet @@ -1542,7 +1547,7 @@ class TimelinePanel extends React.Component { const onLoaded = (): void => { if (this.unmounted) return; - + console.log("loadTimeline -> onLoaded"); // clear the timeline min-height when (re)loading the timeline this.messagePanel.current?.onTimelineReset(); this.reloadEvents(); @@ -1590,6 +1595,7 @@ class TimelinePanel extends React.Component { const onError = (error: MatrixError): void => { if (this.unmounted) return; + console.log("loadTimeline -> onError", error); this.setState({ timelineLoading: false }); logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error); @@ -1644,7 +1650,7 @@ class TimelinePanel extends React.Component { onLoaded(); return; } - + console.log("calling timelineWindow.load"); const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise => { if (this.overlayTimelineWindow) { // TODO: use timestampToEvent to load the overlay timeline @@ -1685,6 +1691,7 @@ class TimelinePanel extends React.Component { // get the list of events from the timeline windows and the pending event list private getEvents(): Pick { + console.log("getEvents"); const mainEvents = this.timelineWindow!.getEvents(); let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; if (this.props.overlayTimelineSetFilter !== undefined) { From ee85f73ad1164769fc9f959f04a7b400dfded205 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:10:26 +0100 Subject: [PATCH 06/40] Control the race condition when swapping between rooms --- src/SlidingSyncManager.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 6f663167695..7231ca2108a 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -36,7 +36,7 @@ Please see LICENSE files in the repository root for full details. * list ops) */ -import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, EventType, ClientEvent, Room } from "matrix-js-sdk/src/matrix"; import { MSC3575Filter, MSC3575List, @@ -232,29 +232,35 @@ export class SlidingSyncManager { subscriptions.delete(roomId); } const room = this.client?.getRoom(roomId); - let shouldLazyLoad = !this.client?.isRoomEncrypted(roomId); - if (!room) { - // default to safety: request all state if we can't work it out. This can happen if you - // refresh the app whilst viewing a room: we call setRoomVisible before we know anything - // about the room. - shouldLazyLoad = false; + // default to safety: request all state if we can't work it out. This can happen if you + // refresh the app whilst viewing a room: we call setRoomVisible before we know anything + // about the room. + let shouldLazyLoad = false; + if (room) { + // do not lazy load encrypted rooms as we need the entire member list. + shouldLazyLoad = !room.hasEncryptionStateEvent() } logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad); if (shouldLazyLoad) { // lazy load this room this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME); } - const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions); + this.slidingSync!.modifyRoomSubscriptions(subscriptions); if (room) { return roomId; // we have data already for this room, show immediately e.g it's in a list } - try { - // wait until the next sync before returning as RoomView may need to know the current state - await p; - } catch (err) { - logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction"); - } - return roomId; + // wait until we know about this room. This may take a little while. + return new Promise((resolve) => { + logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`); + const waitForRoom = (r: Room) => { + if (r.roomId === roomId) { + this.client?.off(ClientEvent.Room, waitForRoom); + logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`); + resolve(roomId); + } + }; + this.client?.on(ClientEvent.Room, waitForRoom); + }); } /** From be768525295c024eb9c1a29904ab59007ccccc31 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:31:35 +0100 Subject: [PATCH 07/40] Dont' filter by space as synapse doesn't support it --- src/stores/room-list/SlidingRoomListStore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts index 26d3291625c..e87d12dcf86 100644 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ b/src/stores/room-list/SlidingRoomListStore.ts @@ -337,7 +337,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl } private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean): void => { - logger.info("SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace); + logger.info("TODO: Synapse does not implement filters.spaces yet. SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace); + return; // update the untagged filter const tagId = DefaultTagID.Untagged; const filters = filterConditions[tagId]; From 0b142b496d4e23434e1a16762ec7ac80a2bed1fd Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:52:55 +0100 Subject: [PATCH 08/40] Remove SS code related to registering lists and managing ranges - Update the spidering code to spider all the relevant lists. - Add canonical alias to the required_state to allow room name calcs to work. Room sort order is busted because we don't yet look at `bump_stamp`. --- src/MatrixClientPeg.ts | 7 + src/SlidingSyncManager.ts | 286 ++++++------- src/components/structures/TimelinePanel.tsx | 10 - src/components/views/rooms/RoomSublist.tsx | 6 - src/hooks/useSlidingSyncRoomSearch.ts | 82 ---- src/stores/room-list/RoomListStore.ts | 14 +- src/stores/room-list/SlidingRoomListStore.ts | 396 ------------------ test/hooks/useSlidingSyncRoomSearch-test.tsx | 85 ---- .../room-list/SlidingRoomListStore-test.ts | 341 --------------- 9 files changed, 149 insertions(+), 1078 deletions(-) delete mode 100644 src/hooks/useSlidingSyncRoomSearch.ts delete mode 100644 src/stores/room-list/SlidingRoomListStore.ts delete mode 100644 test/hooks/useSlidingSyncRoomSearch-test.tsx delete mode 100644 test/stores/room-list/SlidingRoomListStore-test.ts diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 87ad8ec0cb6..2c91ac5a93f 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -42,6 +42,7 @@ import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { Features } from "./settings/Settings"; +import SlidingSyncController from "./settings/controllers/SlidingSyncController"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -298,10 +299,16 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours opts.threadSupport = true; + /* TODO: Uncomment before PR lands if (SettingsStore.getValue("feature_sliding_sync")) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } else { SlidingSyncManager.instance.checkSupport(this.matrixClient); + } */ + // TODO: remove before PR lands. Defaults to SSS if the server entered supports it. + await SlidingSyncManager.instance.checkSupport(this.matrixClient); + if (SlidingSyncController.serverSupportsSlidingSync) { + opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } // Connect the matrix client to the dispatcher and setting handlers diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 7231ca2108a..8ae6917bd21 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -40,10 +40,13 @@ import { MatrixClient, EventType, ClientEvent, Room } from "matrix-js-sdk/src/ma import { MSC3575Filter, MSC3575List, + MSC3575SlidingSyncResponse, MSC3575_STATE_KEY_LAZY, MSC3575_STATE_KEY_ME, MSC3575_WILDCARD, SlidingSync, + SlidingSyncEvent, + SlidingSyncState, } from "matrix-js-sdk/src/sliding-sync"; import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; @@ -53,20 +56,27 @@ import SlidingSyncController from "./settings/controllers/SlidingSyncController" // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; +// The state events we will get for every single room/space/old room/etc +// This list is only augmented when a direct room subscription is made. (e.g you view a room) +const REQUIRED_STATE_LIST = [ + [EventType.RoomJoinRules, ""], // the public icon on the room list + [EventType.RoomAvatar, ""], // any room avatar + [EventType.RoomCanonicalAlias, ""], // for room name calculations + [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead + [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly + [EventType.RoomCreate, ""], // for isSpaceRoom checks + [EventType.SpaceChild, MSC3575_WILDCARD], // all space children + [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents + [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room +]; + // the things to fetch when a user clicks on a room const DEFAULT_ROOM_SUBSCRIPTION_INFO = { timeline_limit: 50, // missing required_state which will change depending on the kind of room include_old_rooms: { timeline_limit: 0, - required_state: [ - // state needed to handle space navigation and tombstone chains - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], - [EventType.SpaceChild, MSC3575_WILDCARD], - [EventType.SpaceParent, MSC3575_WILDCARD], - [EventType.RoomMember, MSC3575_STATE_KEY_ME], - ], + required_state: REQUIRED_STATE_LIST, }, }; // lazy load room members so rooms like Matrix HQ don't take forever to load @@ -74,7 +84,6 @@ const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted"; const UNENCRYPTED_SUBSCRIPTION = Object.assign( { required_state: [ - [MSC3575_WILDCARD, MSC3575_WILDCARD], // all events [EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership [EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest. ], @@ -93,6 +102,72 @@ const ENCRYPTED_SUBSCRIPTION = Object.assign( DEFAULT_ROOM_SUBSCRIPTION_INFO, ); +// the complete set of lists made in SSS. The manager will spider all of these lists depending +// on the count for each one. +const sssLists: Record = { + spaces: { + ranges: [[0, 20]], + timeline_limit: 0, // we don't care about the most recent message for spaces + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + room_types: ["m.space"], + }, + }, + invites: { + ranges: [[0, 20]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_invite: true, + }, + }, + favourites: { + ranges: [[0, 20]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + tags: ["m.favourite"], + }, + }, + dms: { + ranges: [[0, 20]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_dm: true, + is_invite: false, + // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead + not_tags: ["m.favourite", "m.lowpriority"], + }, + }, + untagged: { + // SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites" + ranges: [[0, 20]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + }, +}; + export type PartialSlidingSyncRequest = { filters?: MSC3575Filter; sort?: string[]; @@ -119,110 +194,27 @@ export class SlidingSyncManager { return SlidingSyncManager.internalInstance; } - public configure(client: MatrixClient, proxyUrl: string): SlidingSync { + private configure(client: MatrixClient, proxyUrl: string): SlidingSync { this.client = client; + // create the set of lists we will use. + const lists = new Map(); + for (let listName in sssLists) { + lists.set(listName, sssLists[listName]); + } // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. this.slidingSync = new SlidingSync( proxyUrl, - new Map(), + lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS, ); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); - // set the space list - this.slidingSync.setList(SlidingSyncManager.ListSpaces, { - ranges: [[0, 20]], - sort: ["by_name"], - slow_get_all_rooms: true, - timeline_limit: 0, - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - include_old_rooms: { - timeline_limit: 0, - required_state: [ - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - }, - filters: { - room_types: ["m.space"], - }, - }); this.configureDefer.resolve(); return this.slidingSync; } - /** - * Ensure that this list is registered. - * @param listKey The list key to register - * @param updateArgs The fields to update on the list. - * @returns The complete list request params - */ - public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise { - logger.debug("ensureListRegistered:::", listKey, updateArgs); - await this.configureDefer.promise; - let list = this.slidingSync!.getListParams(listKey); - if (!list) { - list = { - ranges: [[0, 20]], - sort: ["by_notification_level", "by_recency"], - timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites? - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - include_old_rooms: { - timeline_limit: 0, - required_state: [ - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - }, - }; - list = Object.assign(list, updateArgs); - } else { - const updatedList = Object.assign({}, list, updateArgs); - // cannot use objectHasDiff as we need to do deep diff checking - if (JSON.stringify(list) === JSON.stringify(updatedList)) { - logger.debug("list matches, not sending, update => ", updateArgs); - return list; - } - list = updatedList; - } - - try { - // if we only have range changes then call a different function so we don't nuke the list from before - if (updateArgs.ranges && Object.keys(updateArgs).length === 1) { - this.slidingSync!.setListRanges(listKey, updateArgs.ranges); - } else { - this.slidingSync!.setList(listKey, list); - } - } catch (err) { - logger.debug("ensureListRegistered: update failed txn_id=", err); - } - return this.slidingSync!.getListParams(listKey)!; - } - public async setRoomVisible(roomId: string, visible: boolean): Promise { await this.configureDefer.promise; const subscriptions = this.slidingSync!.getRoomSubscriptions(); @@ -264,57 +256,57 @@ export class SlidingSyncManager { } /** - * Retrieve all rooms on the user's account. Used for pre-populating the local search cache. - * Retrieval is gradual over time. + * Retrieve all rooms on the user's account. Retrieval is gradual over time. + * This function MUST be called BEFORE the first sync request goes out. * @param batchSize The number of rooms to return in each request. * @param gapBetweenRequestsMs The number of milliseconds to wait between requests. */ - public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { - await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - let fetchUpTo = batchSize; - let hasMore = true; - let firstTime = true; - while (hasMore) { - try { - if (firstTime) { - await this.slidingSync!.setList(SlidingSyncManager.ListSearch, { - ranges: [[0, fetchUpTo]], - sort: [ - "by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough - ], - timeline_limit: 0, // we only care about the room details, not messages in the room - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - // we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms - // on the user's account. This means some data in the search dialog results may be inaccurate - // e.g membership of space, but this will be corrected when the user clicks on the room - // as the direct room subscription does include old room iterations. - filters: { - // we get spaces via a different list, so filter them out - not_room_types: ["m.space"], - }, - }); - } else { - await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, [[0,fetchUpTo]]); + private async startSpidering(slidingSync: SlidingSync, batchSize: number, gapBetweenRequestsMs: number): Promise { + // The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously. + // There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than + // potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this + // way, the lists are effectively priority requests. We don't actually care which room goes into which list at this + // point, as the RoomListStore will calculate this based on the returned data. + + // copy the initial set of list names and ranges, we'll keep this map updated. + const listToUpperBound = new Map(Object.keys(sssLists).map((listName) => { + return [listName, sssLists[listName].ranges[0][1]]; + })); + console.log("startSpidering:",listToUpperBound); + + // listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing + // the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/ + // but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes + // from request N. + const lifecycle = async (state: SlidingSyncState, _: MSC3575SlidingSyncResponse | null, err?: Error) => { + if (state !== SlidingSyncState.Complete) { + return; + } + await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors + if (err) { + return; + } + + // for all lists with total counts > range => increase the range + let hasSetRanges = false; + listToUpperBound.forEach((currentUpperBound, listName) => { + const totalCount = slidingSync.getListData(listName)?.joinedCount || 0; + if (currentUpperBound < totalCount) { + // increment the upper bound + const newUpperBound = currentUpperBound + batchSize; + console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`); + listToUpperBound.set(listName, newUpperBound); + // make the next request. This will only send the request when this callback has finished, so if + // we set all the list ranges at once we will only send 1 new request. + slidingSync.setListRanges(listName, [[0,newUpperBound]]); + hasSetRanges = true; } - } catch (err) { - // do nothing, as we reject only when we get interrupted but that's fine as the next - // request will include our data - } finally { - // gradually request more over time, even on errors. - await sleep(gapBetweenRequestsMs); + }) + if (!hasSetRanges) { // finish spidering + slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle); } - const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!; - hasMore = fetchUpTo < listData.joinedCount; - fetchUpTo += batchSize; - firstTime = false; } + slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle); } /** @@ -327,10 +319,10 @@ export class SlidingSyncManager { * @returns A working Sliding Sync or undefined */ public async setup(client: MatrixClient): Promise { - this.configure(client, client.baseUrl); + const slidingSync = this.configure(client, client.baseUrl); logger.info("Simplified Sliding Sync activated at", client.baseUrl); - this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart - return this.slidingSync; + this.startSpidering(slidingSync, 100, 50); // 100 rooms at a time, 50ms apart + return slidingSync; } /** diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 1d71b28c550..f9cfa8faa8b 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -319,7 +319,6 @@ class TimelinePanel extends React.Component { } public componentDidMount(): void { - console.log("componentDidMount"); if (this.props.manageReadReceipts) { this.updateReadReceiptOnUserActivity(); } @@ -365,7 +364,6 @@ class TimelinePanel extends React.Component { } public componentWillUnmount(): void { - console.log("componentWillUnmount"); // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -706,7 +704,6 @@ class TimelinePanel extends React.Component { removed: boolean, data: IRoomTimelineData, ): void => { - console.log("onRoomTimeline ", ev, room, "toStart:",toStartOfTimeline, "removed:",removed,"data:",data); // ignore events for other timeline sets if ( data.timeline.getTimelineSet() !== this.props.timelineSet && @@ -812,7 +809,6 @@ class TimelinePanel extends React.Component { } private onRoomTimelineReset = (room: Room | undefined, timelineSet: EventTimelineSet): void => { - console.log("onRoomTimelineReset", room, timelineSet); if (timelineSet !== this.props.timelineSet && timelineSet !== this.props.overlayTimelineSet) return; if (this.canResetTimeline()) { @@ -1428,7 +1424,6 @@ class TimelinePanel extends React.Component { }; private initTimeline(props: IProps): void { - console.log("initTimeline"); const initialEvent = props.eventId; const pixelOffset = props.eventPixelOffset; @@ -1538,7 +1533,6 @@ class TimelinePanel extends React.Component { * @param {boolean?} scrollIntoView whether to scroll the event into view. */ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void { - console.log("loadTimeline"); const cli = MatrixClientPeg.safeGet(); this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap }); this.overlayTimelineWindow = this.props.overlayTimelineSet @@ -1547,7 +1541,6 @@ class TimelinePanel extends React.Component { const onLoaded = (): void => { if (this.unmounted) return; - console.log("loadTimeline -> onLoaded"); // clear the timeline min-height when (re)loading the timeline this.messagePanel.current?.onTimelineReset(); this.reloadEvents(); @@ -1595,7 +1588,6 @@ class TimelinePanel extends React.Component { const onError = (error: MatrixError): void => { if (this.unmounted) return; - console.log("loadTimeline -> onError", error); this.setState({ timelineLoading: false }); logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error); @@ -1650,7 +1642,6 @@ class TimelinePanel extends React.Component { onLoaded(); return; } - console.log("calling timelineWindow.load"); const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise => { if (this.overlayTimelineWindow) { // TODO: use timestampToEvent to load the overlay timeline @@ -1691,7 +1682,6 @@ class TimelinePanel extends React.Component { // get the list of events from the timeline windows and the pending event list private getEvents(): Pick { - console.log("getEvents"); const mainEvents = this.timelineWindow!.getEvents(); let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; if (this.props.overlayTimelineSetFilter !== undefined) { diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 12f6e70d31c..8c928f51e32 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -329,12 +329,6 @@ export default class RoomSublist extends React.Component { }; private onShowAllClick = async (): Promise => { - if (this.slidingSyncMode) { - const count = RoomListStore.instance.getCount(this.props.tagId); - await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, { - ranges: [[0, count]], - }); - } // read number of visible tiles before we mutate it const numVisibleTiles = this.numVisibleTiles; const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding); diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts deleted file mode 100644 index bd8529bfd97..00000000000 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useCallback, useState } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../MatrixClientPeg"; -import { useLatestResult } from "./useLatestResult"; -import { SlidingSyncManager } from "../SlidingSyncManager"; - -export interface SlidingSyncRoomSearchOpts { - limit: number; - query: string; -} - -export const useSlidingSyncRoomSearch = (): { - loading: boolean; - rooms: Room[]; - search(opts: SlidingSyncRoomSearchOpts): Promise; -} => { - const [rooms, setRooms] = useState([]); - - const [loading, setLoading] = useState(false); - - const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms); - - const search = useCallback( - async ({ limit = 100, query: term }: SlidingSyncRoomSearchOpts): Promise => { - const opts = { limit, term }; - updateQuery(opts); - - if (!term?.length) { - setRooms([]); - return true; - } - - try { - setLoading(true); - await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, { - ranges: [[0, limit]], - filters: { - room_name_like: term, - }, - }); - const rooms: Room[] = []; - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync!.getListData( - SlidingSyncManager.ListSearch, - )!; - let i = 0; - while (roomIndexToRoomId[i]) { - const roomId = roomIndexToRoomId[i]; - const room = MatrixClientPeg.safeGet().getRoom(roomId); - if (room) { - rooms.push(room); - } - i++; - } - updateResult(opts, rooms); - return true; - } catch (e) { - console.error("Could not fetch sliding sync rooms for params", { limit, term }, e); - updateResult(opts, []); - return false; - } finally { - setLoading(false); - // TODO: delete the list? - } - }, - [updateQuery, updateResult], - ); - - return { - loading, - rooms, - search, - } as const; -}; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 53377e0a01f..1b123a316d9 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -27,7 +27,6 @@ import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators"; import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; -import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; import { getChangedOverrideRoomMutePushRules } from "./utils/roomMute"; @@ -640,16 +639,9 @@ export default class RoomListStore { public static get instance(): Interface { if (!RoomListStore.internalInstance) { - if (SettingsStore.getValue("feature_sliding_sync")) { - logger.info("using SlidingRoomListStoreClass"); - const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance); - instance.start(); - RoomListStore.internalInstance = instance; - } else { - const instance = new RoomListStoreClass(defaultDispatcher); - instance.start(); - RoomListStore.internalInstance = instance; - } + const instance = new RoomListStoreClass(defaultDispatcher); + instance.start(); + RoomListStore.internalInstance = instance; } return this.internalInstance; diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts deleted file mode 100644 index e87d12dcf86..00000000000 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ /dev/null @@ -1,396 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; -import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync"; -import { Optional } from "matrix-events-sdk"; - -import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models"; -import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { MatrixDispatcher } from "../../dispatcher/dispatcher"; -import { IFilterCondition } from "./filters/IFilterCondition"; -import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; -import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces"; -import { LISTS_LOADING_EVENT } from "./RoomListStore"; -import { UPDATE_EVENT } from "../AsyncStore"; -import { SdkContextClass } from "../../contexts/SDKContext"; - -interface IState { - // state is tracked in underlying classes -} - -export const SlidingSyncSortToFilter: Record = { - [SortAlgorithm.Alphabetic]: ["by_name", "by_recency"], - [SortAlgorithm.Recent]: ["by_notification_level", "by_recency"], - [SortAlgorithm.Manual]: ["by_recency"], -}; - -const filterConditions: Record = { - [DefaultTagID.Invite]: { - is_invite: true, - }, - [DefaultTagID.Favourite]: { - tags: ["m.favourite"], - }, - [DefaultTagID.DM]: { - is_dm: true, - is_invite: false, - // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead - not_tags: ["m.favourite", "m.lowpriority"], - }, - [DefaultTagID.Untagged]: { - is_dm: false, - is_invite: false, - not_room_types: ["m.space"], - not_tags: ["m.favourite", "m.lowpriority"], - // spaces filter added dynamically - }, - [DefaultTagID.LowPriority]: { - tags: ["m.lowpriority"], - // If a room has both Favourite & Low Prio tags then it'll be shown under Favourites - not_tags: ["m.favourite"], - }, - // TODO https://github.com/vector-im/element-web/issues/23207 - // DefaultTagID.ServerNotice, - // DefaultTagID.Suggested, - // DefaultTagID.Archived, -}; - -export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate; - -export class SlidingRoomListStoreClass extends AsyncStoreWithClient implements Interface { - private tagIdToSortAlgo: Record = {}; - private tagMap: ITagMap = {}; - private counts: Record = {}; - private stickyRoomId: Optional; - - public constructor( - dis: MatrixDispatcher, - private readonly context: SdkContextClass, - ) { - super(dis); - this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares - } - - public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise { - logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort); - this.tagIdToSortAlgo[tagId] = sort; - switch (sort) { - case SortAlgorithm.Alphabetic: - await this.context.slidingSyncManager.ensureListRegistered(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], - }); - break; - case SortAlgorithm.Recent: - await this.context.slidingSyncManager.ensureListRegistered(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], - }); - break; - case SortAlgorithm.Manual: - logger.error("cannot enable manual sort in sliding sync mode"); - break; - default: - logger.error("unknown sort mode: ", sort); - } - } - - public getTagSorting(tagId: TagID): SortAlgorithm { - let algo = this.tagIdToSortAlgo[tagId]; - if (!algo) { - logger.warn("SlidingRoomListStore.getTagSorting: no sort algorithm for tag ", tagId); - algo = SortAlgorithm.Recent; // why not, we have to do something.. - } - return algo; - } - - public getCount(tagId: TagID): number { - return this.counts[tagId] || 0; - } - - public setListOrder(tagId: TagID, order: ListAlgorithm): void { - // TODO: https://github.com/vector-im/element-web/issues/23207 - } - - public getListOrder(tagId: TagID): ListAlgorithm { - // TODO: handle unread msgs first? https://github.com/vector-im/element-web/issues/23207 - return ListAlgorithm.Natural; - } - - /** - * Adds a filter condition to the room list store. Filters may be applied async, - * and thus might not cause an update to the store immediately. - * @param {IFilterCondition} filter The filter condition to add. - */ - public async addFilter(filter: IFilterCondition): Promise { - // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear - // in the room list. We do not support arbitrary code for filters in sliding sync. - } - - /** - * Removes a filter condition from the room list store. If the filter was - * not previously added to the room list store, this will no-op. The effects - * of removing a filter may be applied async and therefore might not cause - * an update right away. - * @param {IFilterCondition} filter The filter condition to remove. - */ - public removeFilter(filter: IFilterCondition): void { - // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear - // in the room list. We do not support arbitrary code for filters in sliding sync. - } - - /** - * Gets the tags for a room identified by the store. The returned set - * should never be empty, and will contain DefaultTagID.Untagged if - * the store is not aware of any tags. - * @param room The room to get the tags for. - * @returns The tags for the room. - */ - public getTagsForRoom(room: Room): TagID[] { - // check all lists for each tag we know about and see if the room is there - const tags: TagID[] = []; - for (const tagId in this.tagIdToSortAlgo) { - const listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId); - if (!listData) { - continue; - } - for (const roomIndex in listData.roomIndexToRoomId) { - const roomId = listData.roomIndexToRoomId[roomIndex]; - if (roomId === room.roomId) { - tags.push(tagId); - break; - } - } - } - return tags; - } - - /** - * Manually update a room with a given cause. This should only be used if the - * room list store would otherwise be incapable of doing the update itself. Note - * that this may race with the room list's regular operation. - * @param {Room} room The room to update. - * @param {RoomUpdateCause} cause The cause to update for. - */ - public async manualRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - // TODO: this is only used when you forget a room, not that important for now. - } - - public get orderedLists(): ITagMap { - return this.tagMap; - } - - private refreshOrderedLists(tagId: string, roomIndexToRoomId: Record): void { - const tagMap = this.tagMap; - - // this room will not move due to it being viewed: it is sticky. This can be null to indicate - // no sticky room if you aren't viewing a room. - this.stickyRoomId = this.context.roomViewStore.getRoomId(); - let stickyRoomNewIndex = -1; - const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room): boolean => { - return room.roomId === this.stickyRoomId; - }); - - // order from low to high - const orderedRoomIndexes = Object.keys(roomIndexToRoomId) - .map((numStr) => { - return Number(numStr); - }) - .sort((a, b) => { - return a - b; - }); - const seenRoomIds = new Set(); - const orderedRoomIds = orderedRoomIndexes.map((i) => { - const rid = roomIndexToRoomId[i]; - if (seenRoomIds.has(rid)) { - logger.error("room " + rid + " already has an index position: duplicate room!"); - } - seenRoomIds.add(rid); - if (!rid) { - throw new Error("index " + i + " has no room ID: Map => " + JSON.stringify(roomIndexToRoomId)); - } - if (rid === this.stickyRoomId) { - stickyRoomNewIndex = i; - } - return rid; - }); - logger.debug( - `SlidingRoomListStore.refreshOrderedLists ${tagId} sticky: ${this.stickyRoomId}`, - `${stickyRoomOldIndex} -> ${stickyRoomNewIndex}`, - "rooms:", - orderedRoomIds.length < 30 ? orderedRoomIds : orderedRoomIds.length, - ); - - if (this.stickyRoomId && stickyRoomOldIndex >= 0 && stickyRoomNewIndex >= 0) { - // this update will move this sticky room from old to new, which we do not want. - // Instead, keep the sticky room ID index position as it is, swap it with - // whatever was in its place. - // Some scenarios with sticky room S and bump room B (other letters unimportant): - // A, S, C, B S, A, B - // B, A, S, C <---- without sticky rooms ---> B, S, A - // B, S, A, C <- with sticky rooms applied -> S, B, A - // In other words, we need to swap positions to keep it locked in place. - const inWayRoomId = orderedRoomIds[stickyRoomOldIndex]; - orderedRoomIds[stickyRoomOldIndex] = this.stickyRoomId; - orderedRoomIds[stickyRoomNewIndex] = inWayRoomId; - } - - // now set the rooms - const rooms: Room[] = []; - orderedRoomIds.forEach((roomId) => { - const room = this.matrixClient?.getRoom(roomId); - if (!room) { - return; - } - rooms.push(room); - }); - tagMap[tagId] = rooms; - this.tagMap = tagMap; - } - - private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record): void { - this.counts[tagId] = joinCount; - this.refreshOrderedLists(tagId, roomIndexToRoomId); - // let the UI update - this.emit(LISTS_UPDATE_EVENT); - } - - private onRoomViewStoreUpdated(): void { - // we only care about this to know when the user has clicked on a room to set the stickiness value - if (this.context.roomViewStore.getRoomId() === this.stickyRoomId) { - return; - } - - let hasUpdatedAnyList = false; - - // every list with the OLD sticky room ID needs to be resorted because it now needs to take - // its proper place as it is no longer sticky. The newly sticky room can remain the same though, - // as we only actually care about its sticky status when we get list updates. - const oldStickyRoom = this.stickyRoomId; - // it's not safe to check the data in slidingSync as it is tracking the server's view of the - // room list. There's an edge case whereby the sticky room has gone outside the window and so - // would not be present in the roomIndexToRoomId map anymore, and hence clicking away from it - // will make it disappear eventually. We need to check orderedLists as that is the actual - // sorted renderable list of rooms which sticky rooms apply to. - for (const tagId in this.orderedLists) { - const list = this.orderedLists[tagId]; - const room = list.find((room) => { - return room.roomId === oldStickyRoom; - }); - if (room) { - // resort it based on the slidingSync view of the list. This may cause this old sticky - // room to cease to exist. - const listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId); - if (!listData) { - continue; - } - this.refreshOrderedLists(tagId, listData.roomIndexToRoomId); - hasUpdatedAnyList = true; - } - } - // in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID. - this.stickyRoomId = this.context.roomViewStore.getRoomId(); - - if (hasUpdatedAnyList) { - this.emit(LISTS_UPDATE_EVENT); - } - } - - protected async onReady(): Promise { - logger.info("SlidingRoomListStore.onReady"); - // permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation. - this.context.slidingSyncManager.slidingSync!.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this)); - this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); - this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this)); - if (this.context.spaceStore.activeSpace) { - this.onSelectedSpaceUpdated(this.context.spaceStore.activeSpace, false); - } - - // sliding sync has an initial response for spaces. Now request all the lists. - // We do the spaces list _first_ to avoid potential flickering on DefaultTagID.Untagged list - // which would be caused by initially having no `spaces` filter set, and then suddenly setting one. - OrderedDefaultTagIDs.forEach((tagId) => { - const filter = filterConditions[tagId]; - if (!filter) { - logger.info("SlidingRoomListStore.onReady unsupported list ", tagId); - return; // we do not support this list yet. - } - const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config - this.tagIdToSortAlgo[tagId] = sort; - this.emit(LISTS_LOADING_EVENT, tagId, true); - this.context.slidingSyncManager - .ensureListRegistered(tagId, { - filters: filter, - sort: SlidingSyncSortToFilter[sort], - }) - .then(() => { - this.emit(LISTS_LOADING_EVENT, tagId, false); - }); - }); - } - - private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean): void => { - logger.info("TODO: Synapse does not implement filters.spaces yet. SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace); - return; - // update the untagged filter - const tagId = DefaultTagID.Untagged; - const filters = filterConditions[tagId]; - const oldSpace = filters.spaces?.[0]; - filters.spaces = activeSpace && activeSpace != MetaSpace.Home ? [activeSpace] : undefined; - if (oldSpace !== activeSpace) { - // include subspaces in this list - this.context.spaceStore.traverseSpace( - activeSpace, - (roomId: string) => { - if (roomId === activeSpace) { - return; - } - if (!filters.spaces) { - filters.spaces = []; - } - filters.spaces.push(roomId); // add subspace - }, - false, - ); - - this.emit(LISTS_LOADING_EVENT, tagId, true); - this.context.slidingSyncManager - .ensureListRegistered(tagId, { - filters: filters, - }) - .then(() => { - this.emit(LISTS_LOADING_EVENT, tagId, false); - }); - } - }; - - // Intended for test usage - public async resetStore(): Promise { - // Test function - } - - /** - * Regenerates the room whole room list, discarding any previous results. - * - * Note: This is only exposed externally for the tests. Do not call this from within - * the app. - * @param trigger Set to false to prevent a list update from being sent. Should only - * be used if the calling code will manually trigger the update. - */ - public regenerateAllLists({ trigger = true }): void { - // Test function - } - - protected async onNotReady(): Promise { - await this.resetStore(); - } - - protected async onAction(payload: ActionPayload): Promise {} -} diff --git a/test/hooks/useSlidingSyncRoomSearch-test.tsx b/test/hooks/useSlidingSyncRoomSearch-test.tsx deleted file mode 100644 index 003fa2cafe1..00000000000 --- a/test/hooks/useSlidingSyncRoomSearch-test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { waitFor } from "@testing-library/react"; -import { renderHook, act } from "@testing-library/react-hooks/dom"; -import { mocked } from "jest-mock"; -import { SlidingSync } from "matrix-js-sdk/src/sliding-sync"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import { useSlidingSyncRoomSearch } from "../../src/hooks/useSlidingSyncRoomSearch"; -import { MockEventEmitter, stubClient } from "../test-utils"; -import { SlidingSyncManager } from "../../src/SlidingSyncManager"; - -describe("useSlidingSyncRoomSearch", () => { - afterAll(() => { - jest.restoreAllMocks(); - }); - - it("should display rooms when searching", async () => { - const client = stubClient(); - const roomA = new Room("!a:localhost", client, client.getUserId()!); - const roomB = new Room("!b:localhost", client, client.getUserId()!); - const slidingSync = mocked( - new MockEventEmitter({ - getListData: jest.fn(), - }) as unknown as SlidingSync, - ); - jest.spyOn(SlidingSyncManager.instance, "ensureListRegistered").mockResolvedValue({ - ranges: [[0, 9]], - }); - SlidingSyncManager.instance.slidingSync = slidingSync; - mocked(slidingSync.getListData).mockReturnValue({ - joinedCount: 2, - roomIndexToRoomId: { - 0: roomA.roomId, - 1: roomB.roomId, - }, - }); - mocked(client.getRoom).mockImplementation((roomId) => { - switch (roomId) { - case roomA.roomId: - return roomA; - case roomB.roomId: - return roomB; - default: - return null; - } - }); - - // first check that everything is empty - const { result } = renderHook(() => useSlidingSyncRoomSearch()); - const query = { - limit: 10, - query: "foo", - }; - expect(result.current.loading).toBe(false); - expect(result.current.rooms).toEqual([]); - - // run the query - act(() => { - result.current.search(query); - }); - - // wait for loading to finish - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - // now we expect there to be rooms - expect(result.current.rooms).toEqual([roomA, roomB]); - - // run the query again - act(() => { - result.current.search(query); - }); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - }); -}); diff --git a/test/stores/room-list/SlidingRoomListStore-test.ts b/test/stores/room-list/SlidingRoomListStore-test.ts deleted file mode 100644 index f667ef7dca0..00000000000 --- a/test/stores/room-list/SlidingRoomListStore-test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ -import { mocked } from "jest-mock"; -import { SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import { - LISTS_UPDATE_EVENT, - SlidingRoomListStoreClass, - SlidingSyncSortToFilter, -} from "../../../src/stores/room-list/SlidingRoomListStore"; -import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore"; -import { MockEventEmitter, stubClient, untilEmission } from "../../test-utils"; -import { TestSdkContext } from "../../TestSdkContext"; -import { SlidingSyncManager } from "../../../src/SlidingSyncManager"; -import { RoomViewStore } from "../../../src/stores/RoomViewStore"; -import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; -import { SortAlgorithm } from "../../../src/stores/room-list/algorithms/models"; -import { DefaultTagID, TagID } from "../../../src/stores/room-list/models"; -import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces"; -import { LISTS_LOADING_EVENT } from "../../../src/stores/room-list/RoomListStore"; -import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; - -jest.mock("../../../src/SlidingSyncManager"); -const MockSlidingSyncManager = >(SlidingSyncManager); - -describe("SlidingRoomListStore", () => { - let store: SlidingRoomListStoreClass; - let context: TestSdkContext; - let dis: MatrixDispatcher; - let activeSpace: string; - - beforeEach(async () => { - context = new TestSdkContext(); - context.client = stubClient(); - context._SpaceStore = new MockEventEmitter({ - traverseSpace: jest.fn(), - get activeSpace() { - return activeSpace; - }, - }) as SpaceStoreClass; - context._SlidingSyncManager = new MockSlidingSyncManager(); - context._SlidingSyncManager.slidingSync = mocked( - new MockEventEmitter({ - getListData: jest.fn(), - }) as unknown as SlidingSync, - ); - context._RoomViewStore = mocked( - new MockEventEmitter({ - getRoomId: jest.fn(), - }) as unknown as RoomViewStore, - ); - mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({ - ranges: [[0, 10]], - }); - - dis = new MatrixDispatcher(); - store = new SlidingRoomListStoreClass(dis, context); - }); - - describe("spaces", () => { - it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => { - await store.start(); // call onReady - const spaceRoomId = "!foo:bar"; - - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - - // change the active space - activeSpace = spaceRoomId; - context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); - await p; - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [spaceRoomId], - }), - }); - }); - - it("gracefully handles subspaces in the home metaspace", async () => { - const subspace = "!sub:space"; - mocked(context._SpaceStore!.traverseSpace).mockImplementation( - (spaceId: string, fn: (roomId: string) => void) => { - fn(subspace); - }, - ); - activeSpace = MetaSpace.Home; - await store.start(); // call onReady - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [subspace], - }), - }); - }); - - it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => { - // change the active space before we are ready - const spaceRoomId = "!foo2:bar"; - activeSpace = spaceRoomId; - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - await store.start(); // call onReady - await p; - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith( - DefaultTagID.Untagged, - expect.objectContaining({ - filters: expect.objectContaining({ - spaces: [spaceRoomId], - }), - }), - ); - }); - - it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => { - await store.start(); // call onReady - const spaceRoomId = "!foo:bar"; - const subSpace1 = "!ss1:bar"; - const subSpace2 = "!ss2:bar"; - - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - - mocked(context._SpaceStore!.traverseSpace).mockImplementation( - (spaceId: string, fn: (roomId: string) => void) => { - if (spaceId === spaceRoomId) { - fn(subSpace1); - fn(subSpace2); - } - }, - ); - - // change the active space - activeSpace = spaceRoomId; - context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); - await p; - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [spaceRoomId, subSpace1, subSpace2], - }), - }); - }); - }); - - it("setTagSorting alters the 'sort' option in the list", async () => { - const tagId: TagID = "foo"; - await store.setTagSorting(tagId, SortAlgorithm.Alphabetic); - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], - }); - expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic); - - await store.setTagSorting(tagId, SortAlgorithm.Recent); - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], - }); - expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent); - }); - - it("getTagsForRoom gets the tags for the room", async () => { - await store.start(); - const roomA = "!a:localhost"; - const roomB = "!b:localhost"; - const keyToListData: Record }> = { - [DefaultTagID.Untagged]: { - joinedCount: 10, - roomIndexToRoomId: { - 0: roomA, - 1: roomB, - }, - }, - [DefaultTagID.Favourite]: { - joinedCount: 2, - roomIndexToRoomId: { - 0: roomB, - }, - }, - }; - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - return keyToListData[key] || null; - }); - - expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()!))).toEqual([ - DefaultTagID.Untagged, - ]); - expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()!))).toEqual([ - DefaultTagID.Favourite, - DefaultTagID.Untagged, - ]); - }); - - it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => { - await store.start(); - const roomA = "!a:localhost"; - const roomB = "!b:localhost"; - const roomC = "!c:localhost"; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - const roomIndexToRoomId = { - // mixed to ensure we sort - 1: roomB, - 2: roomC, - 0: roomA, - }; - const rooms = [ - new Room(roomA, context.client!, context.client!.getUserId()!), - new Room(roomB, context.client!, context.client!.getUserId()!), - new Room(roomC, context.client!, context.client!.getUserId()!), - ]; - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomA: - return rooms[0]; - case roomB: - return rooms[1]; - case roomC: - return rooms[2]; - } - return null; - }); - const p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.getCount(tagId)).toEqual(joinCount); - expect(store.orderedLists[tagId]).toEqual(rooms); - }); - - it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => { - await store.start(); - // seed the store with 3 rooms - const roomIdA = "!a:localhost"; - const roomIdB = "!b:localhost"; - const roomIdC = "!c:localhost"; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - const roomIndexToRoomId = { - // mixed to ensure we sort - 1: roomIdB, - 2: roomIdC, - 0: roomIdA, - }; - const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!); - const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()!); - const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!); - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomIdA: - return roomA; - case roomIdB: - return roomB; - case roomIdC: - return roomC; - } - return null; - }); - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - if (key !== tagId) { - return null; - } - return { - roomIndexToRoomId: roomIndexToRoomId, - joinedCount: joinCount, - }; - }); - let p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]); - - // make roomB sticky and inform the store - mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB); - context.roomViewStore.emit(UPDATE_EVENT); - - // bump room C to the top, room B should not move from i=1 despite the list update saying to - roomIndexToRoomId[0] = roomIdC; - roomIndexToRoomId[1] = roomIdA; - roomIndexToRoomId[2] = roomIdB; - p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - - // check that B didn't move and that A was put below B - expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]); - - // make room C sticky: rooms should move as a result, without needing an additional list update - mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC); - p = untilEmission(store, LISTS_UPDATE_EVENT); - context.roomViewStore.emit(UPDATE_EVENT); - await p; - expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId)); - }); - - it("gracefully handles unknown room IDs", async () => { - await store.start(); - const roomIdA = "!a:localhost"; - const roomIdB = "!b:localhost"; // does not exist - const roomIdC = "!c:localhost"; - const roomIndexToRoomId = { - 0: roomIdA, - 1: roomIdB, // does not exist - 2: roomIdC, - }; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - // seed the store with 2 rooms - const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!); - const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!); - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomIdA: - return roomA; - case roomIdC: - return roomC; - } - return null; - }); - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - if (key !== tagId) { - return null; - } - return { - roomIndexToRoomId: roomIndexToRoomId, - joinedCount: joinCount, - }; - }); - const p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.orderedLists[tagId]).toEqual([roomA, roomC]); - }); -}); From 7621fe268266bccb9043797c7b4429a5f1bc5fe4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:11:25 +0100 Subject: [PATCH 09/40] User bumpStamp if it is present --- .../room-list/algorithms/tag-sorting/RecentAlgorithm.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts index d30703d2886..82fe885a5bc 100644 --- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -69,6 +69,12 @@ const getLastTs = (r: Room, userId: string): number => { if (!r?.timeline) { return Number.MAX_SAFE_INTEGER; } + // MSC4186: Simplified Sliding Sync sets this. + // If it's present, sort by it. + const bumpStamp = r.getBumpStamp(); + if (bumpStamp) { + return bumpStamp; + } // If the room hasn't been joined yet, it probably won't have a timeline to // parse. We'll still fall back to the timeline if this fails, but chances From 7662b990a3cf9473e34a78e9d57ee109a58e27c0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:02:59 +0100 Subject: [PATCH 10/40] Drop initial room load from 20 per list to 10 --- src/SlidingSyncManager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 8ae6917bd21..abc6157117a 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -106,7 +106,7 @@ const ENCRYPTED_SUBSCRIPTION = Object.assign( // on the count for each one. const sssLists: Record = { spaces: { - ranges: [[0, 20]], + ranges: [[0, 10]], timeline_limit: 0, // we don't care about the most recent message for spaces required_state: REQUIRED_STATE_LIST, include_old_rooms: { @@ -118,7 +118,7 @@ const sssLists: Record = { }, }, invites: { - ranges: [[0, 20]], + ranges: [[0, 10]], timeline_limit: 1, // most recent message display required_state: REQUIRED_STATE_LIST, include_old_rooms: { @@ -130,7 +130,7 @@ const sssLists: Record = { }, }, favourites: { - ranges: [[0, 20]], + ranges: [[0, 10]], timeline_limit: 1, // most recent message display required_state: REQUIRED_STATE_LIST, include_old_rooms: { @@ -142,7 +142,7 @@ const sssLists: Record = { }, }, dms: { - ranges: [[0, 20]], + ranges: [[0, 10]], timeline_limit: 1, // most recent message display required_state: REQUIRED_STATE_LIST, include_old_rooms: { @@ -158,7 +158,7 @@ const sssLists: Record = { }, untagged: { // SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites" - ranges: [[0, 20]], + ranges: [[0, 10]], timeline_limit: 1, // most recent message display required_state: REQUIRED_STATE_LIST, include_old_rooms: { From 52c2436f4537f6d1d59ad8782253b5aa21e780bf Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:00:01 +0100 Subject: [PATCH 11/40] Half the batch size to trickle more quickly --- src/SlidingSyncManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index abc6157117a..f7f52cb5a10 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -321,7 +321,7 @@ export class SlidingSyncManager { public async setup(client: MatrixClient): Promise { const slidingSync = this.configure(client, client.baseUrl); logger.info("Simplified Sliding Sync activated at", client.baseUrl); - this.startSpidering(slidingSync, 100, 50); // 100 rooms at a time, 50ms apart + this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart return slidingSync; } From 628d2bdadb6ef01fcdd257fdb45eb80739d5955f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2024 16:07:21 +0000 Subject: [PATCH 12/40] Prettier --- src/SlidingSyncManager.ts | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index edc5138fd8c..0bb7c372b8c 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -199,13 +199,7 @@ export class SlidingSyncManager { } // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. - this.slidingSync = new SlidingSync( - proxyUrl, - lists, - ENCRYPTED_SUBSCRIPTION, - client, - SLIDING_SYNC_TIMEOUT_MS, - ); + this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); this.configureDefer.resolve(); return this.slidingSync; @@ -315,7 +309,11 @@ export class SlidingSyncManager { * @param batchSize The number of rooms to return in each request. * @param gapBetweenRequestsMs The number of milliseconds to wait between requests. */ - private async startSpidering(slidingSync: SlidingSync, batchSize: number, gapBetweenRequestsMs: number): Promise { + private async startSpidering( + slidingSync: SlidingSync, + batchSize: number, + gapBetweenRequestsMs: number, + ): Promise { // The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously. // There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than // potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this @@ -323,16 +321,22 @@ export class SlidingSyncManager { // point, as the RoomListStore will calculate this based on the returned data. // copy the initial set of list names and ranges, we'll keep this map updated. - const listToUpperBound = new Map(Object.keys(sssLists).map((listName) => { - return [listName, sssLists[listName].ranges[0][1]]; - })); - console.log("startSpidering:",listToUpperBound); + const listToUpperBound = new Map( + Object.keys(sssLists).map((listName) => { + return [listName, sssLists[listName].ranges[0][1]]; + }), + ); + console.log("startSpidering:", listToUpperBound); // listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing // the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/ // but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes // from request N. - const lifecycle = async (state: SlidingSyncState, _: MSC3575SlidingSyncResponse | null, err?: Error): Promise => { + const lifecycle = async ( + state: SlidingSyncState, + _: MSC3575SlidingSyncResponse | null, + err?: Error, + ): Promise => { if (state !== SlidingSyncState.Complete) { return; } @@ -341,7 +345,7 @@ export class SlidingSyncManager { return; } - // for all lists with total counts > range => increase the range + // for all lists with total counts > range => increase the range let hasSetRanges = false; listToUpperBound.forEach((currentUpperBound, listName) => { const totalCount = slidingSync.getListData(listName)?.joinedCount || 0; @@ -352,11 +356,12 @@ export class SlidingSyncManager { listToUpperBound.set(listName, newUpperBound); // make the next request. This will only send the request when this callback has finished, so if // we set all the list ranges at once we will only send 1 new request. - slidingSync.setListRanges(listName, [[0,newUpperBound]]); + slidingSync.setListRanges(listName, [[0, newUpperBound]]); hasSetRanges = true; } }); - if (!hasSetRanges) { // finish spidering + if (!hasSetRanges) { + // finish spidering slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle); } }; From 473d86120bff0ea862d22af12a8a244c6e00b1d8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2024 16:12:15 +0000 Subject: [PATCH 13/40] prettier on tests too --- test/unit-tests/SlidingSyncManager-test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index 3a515fa3122..80ac19e7664 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -167,9 +167,7 @@ describe("SlidingSyncManager", () => { ); return; } - expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [ - range, - ]); + expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [range]); }); }); it("handles accounts with zero rooms", async () => { @@ -187,9 +185,7 @@ describe("SlidingSyncManager", () => { expect(slidingSync.setList).toHaveBeenCalledWith( SlidingSyncManager.ListSearch, expect.objectContaining({ - ranges: [ - [0, batchSize], - ], + ranges: [[0, batchSize]], }), ); }); From fb1b7e202479a7d0c3f78d27b8863ad76d23b3de Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 12:11:08 +0000 Subject: [PATCH 14/40] Remove proxy URL & unused import --- src/settings/Settings.tsx | 1 - test/unit-tests/SlidingSyncManager-test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 16822f98a01..7ce16df58f9 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -208,7 +208,6 @@ export interface Settings { "feature_ask_to_join": IFeature; "feature_notifications": IFeature; // These are in the feature namespace but aren't actually features - "feature_sliding_sync_proxy_url": IBaseSetting; "feature_hidebold": IBaseSetting; "useOnlyCurrentProfiles": IBaseSetting; diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index 1f1a5c18912..03e6a1e2826 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -14,7 +14,6 @@ import fetchMockJest from "fetch-mock-jest"; import { SlidingSyncManager } from "../../src/SlidingSyncManager"; import { stubClient } from "../test-utils"; import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController"; -import SettingsStore from "../../src/settings/SettingsStore"; jest.mock("matrix-js-sdk/src/sliding-sync"); const MockSlidingSync = >(SlidingSync); From 56906bbfb673dc58b00f3fe0f077e246f498b33b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 17:45:23 +0000 Subject: [PATCH 15/40] Hopefully fix tests to assert what the behaviour is supposed to be --- test/test-utils/test-utils.ts | 1 + test/unit-tests/SlidingSyncManager-test.ts | 71 ++++++++++++---------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index a8b53abb0ee..bd0e7503d8a 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -658,6 +658,7 @@ export function mkStubRoom( getUnreadNotificationCount: jest.fn(() => 0), getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0), getVersion: jest.fn().mockReturnValue("1"), + getBumpStamp: jest.fn().mockReturnValue(0), hasMembershipState: () => false, isElementVideoRoom: jest.fn().mockReturnValue(false), isSpaceRoom: jest.fn().mockReturnValue(false), diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index 03e6a1e2826..fcfcd2642f8 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -6,17 +6,34 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { SlidingSync } from "matrix-js-sdk/src/sliding-sync"; +import { type SlidingSync, SlidingSyncEvent, SlidingSyncState } from "matrix-js-sdk/src/sliding-sync"; import { mocked } from "jest-mock"; import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import fetchMockJest from "fetch-mock-jest"; +import EventEmitter from "events"; +import { waitFor } from "jest-matrix-react"; import { SlidingSyncManager } from "../../src/SlidingSyncManager"; -import { stubClient } from "../test-utils"; +import { mkStubRoom, stubClient } from "../test-utils"; import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController"; -jest.mock("matrix-js-sdk/src/sliding-sync"); -const MockSlidingSync = >(SlidingSync); +//jest.mock("matrix-js-sdk/src/sliding-sync"); +//const MockSlidingSync = >(SlidingSync); +class MockSlidingSync extends EventEmitter { + lists = {}; + listModifiedCount = 0; + terminated = false; + needsResend = false; + modifyRoomSubscriptions = jest.fn(); + getRoomSubscriptions = jest.fn(); + useCustomSubscription = jest.fn(); + getListParams = jest.fn(); + setList = jest.fn(); + setListRanges = jest.fn(); + getListData = jest.fn(); + extensions = jest.fn(); + desiredRoomSubscriptions = jest.fn(); +} describe("SlidingSyncManager", () => { let manager: SlidingSyncManager; @@ -24,7 +41,7 @@ describe("SlidingSyncManager", () => { let client: MatrixClient; beforeEach(() => { - slidingSync = new MockSlidingSync(); + slidingSync = new MockSlidingSync() as unknown as SlidingSync; manager = new SlidingSyncManager(); client = stubClient(); // by default the client has no rooms: stubClient magically makes rooms annoyingly. @@ -38,6 +55,7 @@ describe("SlidingSyncManager", () => { describe("setRoomVisible", () => { it("adds a subscription for the room", async () => { const roomId = "!room:id"; + mocked(client.getRoom).mockReturnValue(mkStubRoom(roomId, "foo", client)); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); await manager.setRoomVisible(roomId, true); @@ -141,7 +159,8 @@ describe("SlidingSyncManager", () => { roomIndexToRoomId: {}, }; }); - await (manager as any).startSpidering(batchSize, gapMs); + await (manager as any).startSpidering(slidingSync, batchSize, gapMs); + // we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69 const wantWindows = [ [0, 10], @@ -152,23 +171,15 @@ describe("SlidingSyncManager", () => { [0, 60], [0, 70], ]; - expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setListRanges).toHaveBeenCalledTimes(wantWindows.length - 1); - wantWindows.forEach((range, i) => { - if (i === 0) { - // eslint-disable-next-line jest/no-conditional-expect - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - // eslint-disable-next-line jest/no-conditional-expect - expect.objectContaining({ - ranges: [range], - }), - ); - return; - } - expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [range]); - }); + + for (let i = 1; i < wantWindows.length; ++i) { + // each time we emit, it should expand the range of all 5 lists by 10 until + // they all include all the rooms (64), which is 6 emits. + slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined); + await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(i * 5)); + expect(slidingSync.setListRanges).toHaveBeenCalledTimes(i * 5); + expect(slidingSync.setListRanges).toHaveBeenCalledWith("spaces", [wantWindows[i]]); + } }); it("handles accounts with zero rooms", async () => { const gapMs = 1; @@ -179,15 +190,11 @@ describe("SlidingSyncManager", () => { roomIndexToRoomId: {}, }; }); - await (manager as any).startSpidering(batchSize, gapMs); - expect(slidingSync.getListData).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - expect.objectContaining({ - ranges: [[0, batchSize]], - }), - ); + await (manager as any).startSpidering(slidingSync, batchSize, gapMs); + slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined); + await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(5)); + // should not have needed to expand the range + expect(slidingSync.setListRanges).not.toHaveBeenCalled(); }); }); describe("checkSupport", () => { From ef990e23b166958ce2a56f2d4a440410ac4b1f30 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 09:56:42 +0000 Subject: [PATCH 16/40] Move the singleton to the manager tyo fix import loop --- src/MatrixClientPeg.ts | 3 +-- src/SlidingSyncManager.ts | 8 ++++---- src/settings/controllers/SlidingSyncController.ts | 7 +++++-- test/unit-tests/SlidingSyncManager-test.ts | 9 +++------ 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 3d77b5a9b15..f2cc5ccdbc8 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -41,7 +41,6 @@ import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; -import SlidingSyncController from "./settings/controllers/SlidingSyncController"; import { initialiseDehydration } from "./utils/device/dehydration"; export interface IMatrixClientCreds { @@ -307,7 +306,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { } */ // TODO: remove before PR lands. Defaults to SSS if the server entered supports it. await SlidingSyncManager.instance.checkSupport(this.matrixClient); - if (SlidingSyncController.serverSupportsSlidingSync) { + if (SlidingSyncManager.serverSupportsSlidingSync) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 896c21e501c..dee9c862bcb 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -51,8 +51,6 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; -import SlidingSyncController from "./settings/controllers/SlidingSyncController"; - // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; @@ -177,6 +175,8 @@ export type PartialSlidingSyncRequest = { * sync options and code. */ export class SlidingSyncManager { + public static serverSupportsSlidingSync: boolean; + public static readonly ListSpaces = "space_list"; public static readonly ListSearch = "search_list"; private static readonly internalInstance = new SlidingSyncManager(); @@ -408,9 +408,9 @@ export class SlidingSyncManager { */ public async checkSupport(client: MatrixClient): Promise { if (await this.nativeSlidingSyncSupport(client)) { - SlidingSyncController.serverSupportsSlidingSync = true; + SlidingSyncManager.serverSupportsSlidingSync = true; return; } - SlidingSyncController.serverSupportsSlidingSync = false; + SlidingSyncManager.serverSupportsSlidingSync = false; } } diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 11ae1a2ba02..0b5e08f0ea7 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -11,9 +11,12 @@ import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; import SettingsStore from "../SettingsStore"; import { _t } from "../../languageHandler"; +import { SlidingSyncManager } from "../../SlidingSyncManager"; export default class SlidingSyncController extends SettingController { - public static serverSupportsSlidingSync: boolean; + public constructor() { + super(); + } public async onChange(): Promise { PlatformPeg.get()?.reload(); @@ -24,7 +27,7 @@ export default class SlidingSyncController extends SettingController { if (SettingsStore.getValue("feature_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } - if (!SlidingSyncController.serverSupportsSlidingSync) { + if (!SlidingSyncManager.serverSupportsSlidingSync) { return _t("labs|sliding_sync_server_no_support"); } diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index fcfcd2642f8..d258863dca5 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -15,10 +15,7 @@ import { waitFor } from "jest-matrix-react"; import { SlidingSyncManager } from "../../src/SlidingSyncManager"; import { mkStubRoom, stubClient } from "../test-utils"; -import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController"; -//jest.mock("matrix-js-sdk/src/sliding-sync"); -//const MockSlidingSync = >(SlidingSync); class MockSlidingSync extends EventEmitter { lists = {}; listModifiedCount = 0; @@ -199,13 +196,13 @@ describe("SlidingSyncManager", () => { }); describe("checkSupport", () => { beforeEach(() => { - SlidingSyncController.serverSupportsSlidingSync = false; + SlidingSyncManager.serverSupportsSlidingSync = false; }); it("shorts out if the server has 'native' sliding sync support", async () => { jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + expect(SlidingSyncManager.serverSupportsSlidingSync).toBeFalsy(); await manager.checkSupport(client); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + expect(SlidingSyncManager.serverSupportsSlidingSync).toBeTruthy(); }); }); describe("setup", () => { From e60b3316b1c9dd820bb31be5d763b5432d501267 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 12:13:16 +0000 Subject: [PATCH 17/40] Very well, code, I will remove you Why were you there in the first place? --- src/MatrixClientPeg.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index f2cc5ccdbc8..e22d9b2cb43 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -298,16 +298,12 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours opts.threadSupport = true; - /* TODO: Uncomment before PR lands + // If the user has enabled the labs feature for sliding sync, set it up + // otherwise check if the feature is supported if (SettingsStore.getValue("feature_sliding_sync")) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } else { SlidingSyncManager.instance.checkSupport(this.matrixClient); - } */ - // TODO: remove before PR lands. Defaults to SSS if the server entered supports it. - await SlidingSyncManager.instance.checkSupport(this.matrixClient); - if (SlidingSyncManager.serverSupportsSlidingSync) { - opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } // Connect the matrix client to the dispatcher and setting handlers From ce6a08097ba991f96f912cf088e5ca97b2753ef8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 16:13:31 +0000 Subject: [PATCH 18/40] Strip out more unused stuff --- src/components/views/rooms/RoomSublist.tsx | 27 ++++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index e9dbdc19fdf..3c54b98b0ea 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -42,8 +42,6 @@ import ContextMenu, { } from "../../structures/ContextMenu"; import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import type ExtraTile from "./ExtraTile"; -import SettingsStore from "../../../settings/SettingsStore"; -import { SlidingSyncManager } from "../../../SlidingSyncManager"; import NotificationBadge from "./NotificationBadge"; import RoomTile from "./RoomTile"; @@ -106,12 +104,8 @@ export default class RoomSublist extends React.Component { private heightAtStart: number; private notificationState: ListNotificationState; - private slidingSyncMode: boolean; - public constructor(props: IProps) { super(props); - // when this setting is toggled it restarts the app so it's safe to not watch this. - this.slidingSyncMode = SettingsStore.getValue("feature_sliding_sync"); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); this.heightAtStart = 0; @@ -165,9 +159,6 @@ export default class RoomSublist extends React.Component { } private get numVisibleTiles(): number { - if (this.slidingSyncMode) { - return this.state.rooms.length; - } const nVisible = Math.ceil(this.layout.visibleTiles); return Math.min(nVisible, this.numTiles); } @@ -548,13 +539,8 @@ export default class RoomSublist extends React.Component { let contextMenu: JSX.Element | undefined; if (this.state.contextMenuPosition) { - let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; - let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; - if (this.slidingSyncMode) { - const slidingList = SlidingSyncManager.instance.slidingSync?.getListParams(this.props.tagId); - isAlphabetical = (slidingList?.sort || [])[0] === "by_name"; - isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level"; - } + const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; // Invites don't get some nonsense options, so only add them if we have to. let otherSections: JSX.Element | undefined; @@ -757,17 +743,12 @@ export default class RoomSublist extends React.Component { // floats above the resize handle, if we have one present. If the user has all // tiles visible, it becomes 'show less'. let showNButton: JSX.Element | undefined; - const hasMoreSlidingSync = - this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length; - if (maxTilesPx > this.state.height || hasMoreSlidingSync) { + if (maxTilesPx > this.state.height) { // the height of all the tiles is greater than the section height: we need a 'show more' button const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT; const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight); - let numMissing = this.numTiles - amountFullyShown; - if (this.slidingSyncMode) { - numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown; - } + const numMissing = this.numTiles - amountFullyShown; const label = _t("room_list|show_n_more", { count: numMissing }); let showMoreText: ReactNode = {label}; if (this.props.isMinimized) showMoreText = null; From 7af6053f6a8719b457206b6e05eb2a2f25d3a2e3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 18:00:11 +0000 Subject: [PATCH 19/40] Fix playwright test Seems like this lack of order updating unless a room is selected was just always a bug with both regular and non-sliding sync. I have no idea how the test passed on develop because it won't run. --- src/stores/room-list/RoomListStore.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 55f18367a88..6985e007bd8 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -405,6 +405,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient implem public setTagSorting(tagId: TagID, sort: SortAlgorithm): void { this.setAndPersistTagSorting(tagId, sort); + // We'll always need an update after changing the sort order, so mark for update and trigger + // immediately. + this.updateFn.mark(); this.updateFn.trigger(); } From 5ce0d0921bb9954d37135db33e3a1ccf8c96f04b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 18:27:55 +0000 Subject: [PATCH 20/40] Fix test to do maybe what it was supposed to do... possibly? --- .../e2e/sliding-sync/sliding-sync.spec.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index bf992a0edd1..b603cdbcc45 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -188,9 +188,25 @@ test.describe("Sliding Sync", () => { ).not.toBeAttached(); }); - test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { + test("should not show unread indicators when the room is muted", async ({ + page, + app, + joinedBot: bot, + testRoom, + }) => { + // XXX Dave - I've edited the name of this test to reflect what I think it was trying to testm + // but then the comment below makes zero sense. I'll leave it in case I'm wrong... // TODO: for now. Later we should. + // Turn message previews on so we can see when the message has arrived + const sublistHeaderLocator = page + .getByRole("group", { name: "Rooms" }) + .locator(".mx_RoomSublist_headerContainer"); + await sublistHeaderLocator.hover(); + await sublistHeaderLocator.getByRole("button", { name: "List options" }).click(); + await page.getByRole("menuitemcheckbox", { name: "Show previews of messages" }).dispatchEvent("click"); + await page.locator(".mx_ContextualMenu_background").click(); + // disable notifs in this room (TODO: CS API call?) const locator = page.getByRole("treeitem", { name: "Test Room" }); await locator.hover(); @@ -204,8 +220,7 @@ test.describe("Sliding Sync", () => { await bot.sendMessage(testRoom.roomId, "Do you read me?"); - // wait for this message to arrive, tell by the room list resorting - await checkOrder(["Test Room", "Dummy"], page); + await expect(page.locator(".mx_RoomTile_subtitle_text")).toHaveText("@bot_0002:localhost: Do you read me?"); await expect( page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"), From 560dc15de6b96e2915f8f5c1b9dc5450bf36f94e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 18:28:42 +0000 Subject: [PATCH 21/40] Remove test for old pre-simplified sliding sync behaviour --- .../e2e/sliding-sync/sliding-sync.spec.ts | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index b603cdbcc45..dbfcf5c2933 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -376,52 +376,4 @@ test.describe("Sliding Sync", () => { // ensure the reply-to does not disappear await expect(page.locator(".mx_ReplyPreview")).toBeVisible(); }); - - test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => { - // create rooms and check room names are correct - const roomIds: string[] = []; - for (const fruit of ["Apple", "Pineapple", "Orange"]) { - const id = await app.client.createRoom({ name: fruit }); - roomIds.push(id); - await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); - } - const [roomAId, roomPId, roomOId] = roomIds; - - const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return body.txn_id && body.room_subscriptions?.[subRoomId]; - }; - const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return ( - body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId] - ); - }; - - // Select the Test Room and wait for playwright to get the request - const [request] = await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomAId)), - page.getByRole("treeitem", { name: "Apple", exact: true }).click(), - ]); - const roomSubscriptions = request.postDataJSON().room_subscriptions; - expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); - - // Switch to another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomPId)), - page.waitForRequest(matchRoomUnsubRequest(roomAId)), - page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), - ]); - - // And switch to even another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomOId)), - page.waitForRequest(matchRoomUnsubRequest(roomPId)), - page.getByRole("treeitem", { name: "Orange", exact: true }).click(), - ]); - - // TODO: Add tests for encrypted rooms - }); }); From 0608f823f03e24920847691edfbc062dbc80757f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Feb 2025 18:35:19 +0000 Subject: [PATCH 22/40] Unused import --- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index dbfcf5c2933..61a3c8e2b05 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Page, type Request } from "@playwright/test"; +import { type Page } from "@playwright/test"; import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; From 527c6361241e4eb8c4bce378e1274c0ad6a8a740 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Feb 2025 15:58:52 +0000 Subject: [PATCH 23/40] Remove sliding sync proxy & test I was wrong about what this test was asserting, it was suposed to assert that notification dots aren't shown (because SS didn't support them somehow I guess) but they are fine in SSS so the test is just no longer relevant. --- .../e2e/sliding-sync/sliding-sync.spec.ts | 70 +------------------ 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 61a3c8e2b05..7dab11afd63 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -7,45 +7,16 @@ Please see LICENSE files in the repository root for full details. */ import { type Page } from "@playwright/test"; -import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { Bot } from "../../pages/bot"; const test = base.extend<{ - slidingSyncProxy: StartedTestContainer; testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => { - const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3") - .withNetwork(network) - .withExposedPorts(8008) - .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) - .withWaitStrategy(Wait.forHttp("/client/server.json", 8008)) - .withEnvironment({ - SYNCV3_SECRET: "bwahahaha", - SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, - SYNCV3_SERVER: `http://homeserver:8008`, - }) - .start(); - - const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`; - await page.addInitScript((proxyAddress) => { - window.localStorage.setItem( - "mx_local_settings", - JSON.stringify({ - feature_sliding_sync_proxy_url: proxyAddress, - }), - ); - window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true"); - }, proxyAddress); - await use(container); - await container.stop(); - }, - // Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script - credentials: async ({ slidingSyncProxy, credentials }, use) => { + credentials: async ({ credentials }, use) => { await use(credentials); }, testRoom: async ({ user, app }, use) => { @@ -188,45 +159,6 @@ test.describe("Sliding Sync", () => { ).not.toBeAttached(); }); - test("should not show unread indicators when the room is muted", async ({ - page, - app, - joinedBot: bot, - testRoom, - }) => { - // XXX Dave - I've edited the name of this test to reflect what I think it was trying to testm - // but then the comment below makes zero sense. I'll leave it in case I'm wrong... - // TODO: for now. Later we should. - - // Turn message previews on so we can see when the message has arrived - const sublistHeaderLocator = page - .getByRole("group", { name: "Rooms" }) - .locator(".mx_RoomSublist_headerContainer"); - await sublistHeaderLocator.hover(); - await sublistHeaderLocator.getByRole("button", { name: "List options" }).click(); - await page.getByRole("menuitemcheckbox", { name: "Show previews of messages" }).dispatchEvent("click"); - await page.locator(".mx_ContextualMenu_background").click(); - - // disable notifs in this room (TODO: CS API call?) - const locator = page.getByRole("treeitem", { name: "Test Room" }); - await locator.hover(); - await locator.getByRole("button", { name: "Notification options" }).click(); - await page.getByRole("menuitemradio", { name: "Mute room" }).click(); - - // create a new room so we know when the message has been received as it'll re-shuffle the room list - await app.client.createRoom({ name: "Dummy" }); - - await checkOrder(["Dummy", "Test Room"], page); - - await bot.sendMessage(testRoom.roomId, "Do you read me?"); - - await expect(page.locator(".mx_RoomTile_subtitle_text")).toHaveText("@bot_0002:localhost: Do you read me?"); - - await expect( - page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"), - ).not.toBeAttached(); - }); - test("should update user settings promptly", async ({ page, app }) => { await app.settings.openUserSettings("Preferences"); const locator = page.locator(".mx_SettingsFlag").filter({ hasText: "Show timestamps in 12 hour format" }); From 9fa612a4b87b691fa42492bd7d10614546dbec02 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Feb 2025 16:39:13 +0000 Subject: [PATCH 24/40] Remove now pointless credentials --- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 7dab11afd63..3e277a79dad 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -16,9 +16,6 @@ const test = base.extend<{ testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - credentials: async ({ credentials }, use) => { - await use(credentials); - }, testRoom: async ({ user, app }, use) => { const name = "Test Room"; const roomId = await app.client.createRoom({ name }); From 9475972ce115a26161795ec16e98b2e4d707023c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 15:17:13 +0000 Subject: [PATCH 25/40] Remove subscription removal as SSS doesn't do that --- src/SlidingSyncManager.ts | 22 ++++++++++++-------- src/stores/RoomViewStore.tsx | 13 ++---------- test/unit-tests/SlidingSyncManager-test.ts | 4 ++-- test/unit-tests/stores/RoomViewStore-test.ts | 16 +++----------- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index dee9c862bcb..88b839312d1 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -263,14 +263,18 @@ export class SlidingSyncManager { return this.slidingSync!.getListParams(listKey)!; } - public async setRoomVisible(roomId: string, visible: boolean): Promise { + /** + * Announces that the user has chosen to view the given room and that room will now + * be displayed, so it should have more state loaded. + * @param roomId The room to set visible + */ + public async setRoomVisible(roomId: string): Promise { await this.configureDefer.promise; const subscriptions = this.slidingSync!.getRoomSubscriptions(); - if (visible) { - subscriptions.add(roomId); - } else { - subscriptions.delete(roomId); - } + if (subscriptions.has(roomId)) return; + + subscriptions.add(roomId); + const room = this.client?.getRoom(roomId); // default to safety: request all state if we can't work it out. This can happen if you // refresh the app whilst viewing a room: we call setRoomVisible before we know anything @@ -280,14 +284,14 @@ export class SlidingSyncManager { // do not lazy load encrypted rooms as we need the entire member list. shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } - logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad); + logger.log("SlidingSync setRoomVisible:", roomId, "shouldLazyLoad:", shouldLazyLoad); if (shouldLazyLoad) { // lazy load this room this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME); } this.slidingSync!.modifyRoomSubscriptions(subscriptions); if (room) { - return roomId; // we have data already for this room, show immediately e.g it's in a list + return; // we have data already for this room, show immediately e.g it's in a list } // wait until we know about this room. This may take a little while. return new Promise((resolve) => { @@ -296,7 +300,7 @@ export class SlidingSyncManager { if (r.roomId === roomId) { this.client?.off(ClientEvent.Room, waitForRoom); logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`); - resolve(roomId); + resolve(); } }; this.client?.on(ClientEvent.Room, waitForRoom); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index c691f0af797..9f15bfe7dd8 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -354,10 +354,6 @@ export class RoomViewStore extends EventEmitter { } if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) { - if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) { - // unsubscribe from this room, but don't await it as we don't care when this gets done. - this.stores.slidingSyncManager.setRoomVisible(this.state.subscribingRoomId, false); - } this.setState({ subscribingRoomId: payload.room_id, roomId: payload.room_id, @@ -373,13 +369,8 @@ export class RoomViewStore extends EventEmitter { }); // set this room as the room subscription. We need to await for it as this will fetch // all room state for this room, which is required before we get the state below. - await this.stores.slidingSyncManager.setRoomVisible(payload.room_id, true); - // Whilst we were subscribing another room was viewed, so stop what we're doing and - // unsubscribe - if (this.state.subscribingRoomId !== payload.room_id) { - this.stores.slidingSyncManager.setRoomVisible(payload.room_id, false); - return; - } + await this.stores.slidingSyncManager.setRoomVisible(payload.room_id); + // Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now this.dis?.dispatch({ ...payload, diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index d258863dca5..5979493427f 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -55,7 +55,7 @@ describe("SlidingSyncManager", () => { mocked(client.getRoom).mockReturnValue(mkStubRoom(roomId, "foo", client)); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - await manager.setRoomVisible(roomId, true); + await manager.setRoomVisible(roomId); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); }); it("adds a custom subscription for a lazy-loadable room", async () => { @@ -80,7 +80,7 @@ describe("SlidingSyncManager", () => { }); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - await manager.setRoomVisible(roomId, true); + await manager.setRoomVisible(roomId); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); // we aren't prescriptive about what the sub name is. expect(slidingSync.useCustomSubscription).toHaveBeenCalledWith(roomId, expect.anything()); diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 74aa952036b..c9274e9368d 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -346,9 +346,7 @@ describe("RoomViewStore", function () { }); it("subscribes to the room", async () => { - const setRoomVisible = jest - .spyOn(slidingSyncManager, "setRoomVisible") - .mockReturnValue(Promise.resolve("")); + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve()); const subscribedRoomId = "!sub1:localhost"; dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }); await untilDispatch(Action.ActiveRoomChanged, dis); @@ -358,26 +356,18 @@ describe("RoomViewStore", function () { // Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode it("doesn't get stuck in a loop if you view rooms quickly", async () => { - const setRoomVisible = jest - .spyOn(slidingSyncManager, "setRoomVisible") - .mockReturnValue(Promise.resolve("")); + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve()); const subscribedRoomId = "!sub1:localhost"; const subscribedRoomId2 = "!sub2:localhost"; dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }, true); dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 }, true); await untilDispatch(Action.ActiveRoomChanged, dis); // sub(1) then unsub(1) sub(2), unsub(1) - const wantCalls = [ - [subscribedRoomId, true], - [subscribedRoomId, false], - [subscribedRoomId2, true], - [subscribedRoomId, false], - ]; + const wantCalls = [[subscribedRoomId], [subscribedRoomId], [subscribedRoomId2], [subscribedRoomId]]; expect(setRoomVisible).toHaveBeenCalledTimes(wantCalls.length); wantCalls.forEach((v, i) => { try { expect(setRoomVisible.mock.calls[i][0]).toEqual(v[0]); - expect(setRoomVisible.mock.calls[i][1]).toEqual(v[1]); } catch { throw new Error(`i=${i} got ${setRoomVisible.mock.calls[i]} want ${v}`); } From 943a45287b810ab5be19c955050868e018bcb239 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 15:27:58 +0000 Subject: [PATCH 26/40] Update tests --- test/unit-tests/stores/RoomViewStore-test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index c9274e9368d..82962a3f5c6 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -351,10 +351,12 @@ describe("RoomViewStore", function () { dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }); await untilDispatch(Action.ActiveRoomChanged, dis); expect(roomViewStore.getRoomId()).toBe(subscribedRoomId); - expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true); + expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId); }); - // Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode + // Previously a regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode + // although that was before the complexity was removed with similified mode. I've removed the complexity but kept the + // test anyway. it("doesn't get stuck in a loop if you view rooms quickly", async () => { const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve()); const subscribedRoomId = "!sub1:localhost"; @@ -362,8 +364,8 @@ describe("RoomViewStore", function () { dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }, true); dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 }, true); await untilDispatch(Action.ActiveRoomChanged, dis); - // sub(1) then unsub(1) sub(2), unsub(1) - const wantCalls = [[subscribedRoomId], [subscribedRoomId], [subscribedRoomId2], [subscribedRoomId]]; + // should view 1, then 2 + const wantCalls = [[subscribedRoomId], [subscribedRoomId2]]; expect(setRoomVisible).toHaveBeenCalledTimes(wantCalls.length); wantCalls.forEach((v, i) => { try { From efd78e1b9af5e76ad326476f586329ddedead245 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 16:25:22 +0000 Subject: [PATCH 27/40] add test --- test/unit-tests/SlidingSyncManager-test.ts | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index 5979493427f..20d9110bcc4 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { type SlidingSync, SlidingSyncEvent, SlidingSyncState } from "matrix-js-sdk/src/sliding-sync"; import { mocked } from "jest-mock"; -import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import fetchMockJest from "fetch-mock-jest"; import EventEmitter from "events"; import { waitFor } from "jest-matrix-react"; @@ -58,6 +58,7 @@ describe("SlidingSyncManager", () => { await manager.setRoomVisible(roomId); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); }); + it("adds a custom subscription for a lazy-loadable room", async () => { const roomId = "!lazy:id"; const room = new Room(roomId, client, client.getUserId()!); @@ -85,6 +86,26 @@ describe("SlidingSyncManager", () => { // we aren't prescriptive about what the sub name is. expect(slidingSync.useCustomSubscription).toHaveBeenCalledWith(roomId, expect.anything()); }); + + it("waits if the room is not yet known", async () => { + const roomId = "!room:id"; + mocked(client.getRoom).mockReturnValue(null); + const subs = new Set(); + mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); + + const setVisibleDone = jest.fn(); + manager.setRoomVisible(roomId).then(setVisibleDone); + + await waitFor(() => expect(client.getRoom).toHaveBeenCalledWith(roomId)); + + expect(setVisibleDone).not.toHaveBeenCalled(); + + const stubRoom = mkStubRoom(roomId, "foo", client); + mocked(client.getRoom).mockReturnValue(stubRoom); + client.emit(ClientEvent.Room, stubRoom); + + await waitFor(() => expect(setVisibleDone).toHaveBeenCalled()); + }); }); describe("ensureListRegistered", () => { From 81507f3f95da9692e090658cb597dc64d30b87aa Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 16:53:26 +0000 Subject: [PATCH 28/40] Switch to new labs flag & break if old labs flag is enabled --- src/MatrixClientPeg.ts | 8 +++++++- src/Unread.ts | 6 ------ src/settings/Settings.tsx | 12 ++++++++++++ src/settings/controllers/SlidingSyncController.ts | 2 +- src/stores/MemberListStore.ts | 4 ++-- src/stores/RoomViewStore.tsx | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 1625b9eaa95..3e8ed8c8092 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -298,9 +298,15 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours opts.threadSupport = true; + if (SettingsStore.getValue("feature_sliding_sync")) { + throw new Error( + "Legacy sliding sync is no longer supported: please log out and back in to enable the new sliding sync flag", + ); + } + // If the user has enabled the labs feature for sliding sync, set it up // otherwise check if the feature is supported - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } else { SlidingSyncManager.instance.checkSupport(this.matrixClient); diff --git a/src/Unread.ts b/src/Unread.ts index 2c8fa0cff39..472f6f2702e 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -44,12 +44,6 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent): } export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean { - if (SettingsStore.getValue("feature_sliding_sync")) { - // TODO: https://github.com/vector-im/element-web/issues/23207 - // Sliding Sync doesn't support unread indicator dots (yet...) - return false; - } - const toCheck: Array = [room]; if (includeThreads) { toCheck.push(...room.getThreads()); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0f0fc00d32b..5acdc1aa61d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -198,6 +198,7 @@ export interface Settings { "feature_bridge_state": IFeature; "feature_jump_to_date": IFeature; "feature_sliding_sync": IFeature; + "feature_simplified_sliding_sync": IFeature; "feature_element_call_video_rooms": IFeature; "feature_group_calls": IFeature; "feature_disable_call_per_sender_encryption": IFeature; @@ -533,6 +534,7 @@ export const SETTINGS: Settings = { true, ), }, + // legacy sliding sync flag: no longer works, will error for anyone who's still using it "feature_sliding_sync": { isFeature: true, labsGroup: LabGroup.Developer, @@ -542,6 +544,16 @@ export const SETTINGS: Settings = { description: _td("labs|sliding_sync_description"), shouldWarn: true, default: false, + }, + "feature_simplified_sliding_sync": { + isFeature: true, + labsGroup: LabGroup.Developer, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, + supportedLevelsAreOrdered: true, + displayName: _td("labs|sliding_sync"), + description: _td("labs|sliding_sync_description"), + shouldWarn: true, + default: false, controller: new SlidingSyncController(), }, "feature_element_call_video_rooms": { diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 0b5e08f0ea7..165be14a269 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -24,7 +24,7 @@ export default class SlidingSyncController extends SettingController { public get settingDisabled(): boolean | string { // Cannot be disabled once enabled, user has been warned and must log out and back in. - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } if (!SlidingSyncManager.serverSupportsSlidingSync) { diff --git a/src/stores/MemberListStore.ts b/src/stores/MemberListStore.ts index 04c5824d998..29269bac943 100644 --- a/src/stores/MemberListStore.ts +++ b/src/stores/MemberListStore.ts @@ -122,7 +122,7 @@ export class MemberListStore { * @returns True if enabled */ private async isLazyLoadingEnabled(roomId: string): Promise { - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { // only unencrypted rooms use lazy loading return !(await this.stores.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } @@ -134,7 +134,7 @@ export class MemberListStore { * @returns True if there is storage for lazy loading members */ private isLazyMemberStorageEnabled(): boolean { - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { return false; } return this.stores.client!.hasLazyLoadMembersEnabled(); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 9f15bfe7dd8..a49b94d49e8 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -353,7 +353,7 @@ export class RoomViewStore extends EventEmitter { }); } - if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) { + if (SettingsStore.getValue("feature_simplified_sliding_sync") && this.state.roomId !== payload.room_id) { this.setState({ subscribingRoomId: payload.room_id, roomId: payload.room_id, From 0bbd2d390dad304af4ffda294412601a017ae428 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 17:05:14 +0000 Subject: [PATCH 29/40] Remove unused import & fix test --- src/Unread.ts | 1 - test/unit-tests/stores/MemberListStore-test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Unread.ts b/src/Unread.ts index 472f6f2702e..e8f4769e25f 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; -import SettingsStore from "./settings/SettingsStore"; import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs"; /** diff --git a/test/unit-tests/stores/MemberListStore-test.ts b/test/unit-tests/stores/MemberListStore-test.ts index 883bb13f110..9139dde85d7 100644 --- a/test/unit-tests/stores/MemberListStore-test.ts +++ b/test/unit-tests/stores/MemberListStore-test.ts @@ -161,7 +161,7 @@ describe("MemberListStore", () => { describe("sliding sync", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { - return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled. + return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled. }); client.members = jest.fn(); }); From 084fe59138c7d2e7679de13e59bb9d36c3c33de9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 17:12:37 +0000 Subject: [PATCH 30/40] Fix other test --- test/unit-tests/stores/RoomViewStore-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 82962a3f5c6..1d7b6366cc4 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -341,7 +341,7 @@ describe("RoomViewStore", function () { describe("Sliding Sync", function () { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { - return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled. + return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled. }); }); From 8a52c49e3ff5a4c32d51eac040914db8befb2b8d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 19 Feb 2025 17:37:53 +0000 Subject: [PATCH 31/40] Remove name & description from old labs flag as they're not displayed anywhere so not useful --- src/settings/Settings.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5acdc1aa61d..a58e045e034 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -540,8 +540,6 @@ export const SETTINGS: Settings = { labsGroup: LabGroup.Developer, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, - displayName: _td("labs|sliding_sync"), - description: _td("labs|sliding_sync_description"), shouldWarn: true, default: false, }, From 3d6afc7b9693809dd17bf95eb9f769dc779e38d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Mar 2025 13:48:09 +0000 Subject: [PATCH 32/40] Remove old sliding sync option by making it not a feature --- src/settings/Settings.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a58e045e034..aed1606ddfd 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -197,7 +197,7 @@ export interface Settings { "feature_html_topic": IFeature; "feature_bridge_state": IFeature; "feature_jump_to_date": IFeature; - "feature_sliding_sync": IFeature; + "feature_sliding_sync": IBaseSetting; "feature_simplified_sliding_sync": IFeature; "feature_element_call_video_rooms": IFeature; "feature_group_calls": IFeature; @@ -536,8 +536,6 @@ export const SETTINGS: Settings = { }, // legacy sliding sync flag: no longer works, will error for anyone who's still using it "feature_sliding_sync": { - isFeature: true, - labsGroup: LabGroup.Developer, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, shouldWarn: true, From 3777116dd706b54dc6edc67ab7269486190f660e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 13:55:56 +0000 Subject: [PATCH 33/40] Add back unread nindicator test but inverted and minus the bit about disabling notification which surely would have defeated the original point anyway? --- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 3e277a79dad..539bbfdd114 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -156,6 +156,20 @@ test.describe("Sliding Sync", () => { ).not.toBeAttached(); }); + test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { + // create a new room so we know when the message has been received as it'll re-shuffle the room list + await app.client.createRoom({ name: "Dummy" }); + + await checkOrder(["Dummy", "Test Room"], page); + + await bot.sendMessage(testRoom.roomId, "Do you read me?"); + + // wait for this message to arrive, tell by the room list resorting + await checkOrder(["Test Room", "Dummy"], page); + + await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached(); + }); + test("should update user settings promptly", async ({ page, app }) => { await app.settings.openUserSettings("Preferences"); const locator = page.locator(".mx_SettingsFlag").filter({ hasText: "Show timestamps in 12 hour format" }); From fc41b54130c4f9543ff5e34fa4c37f53dd8e0fbb Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 14:37:57 +0000 Subject: [PATCH 34/40] Reinstate test for room_subscriptions ...and also make tests actually use sliding sync --- .../e2e/sliding-sync/sliding-sync.spec.ts | 41 ++++++++++++++++++- playwright/element-web-test.ts | 1 + 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 539bbfdd114..118bd4585ee 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Page } from "@playwright/test"; +import { type Page, type Request } from "@playwright/test"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; @@ -50,6 +50,14 @@ test.describe("Sliding Sync", () => { }); }; + test.use({ + config: { + features: { + feature_simplified_sliding_sync: true, + }, + }, + }); + // Load the user fixture for all tests test.beforeEach(({ user }) => {}); @@ -179,6 +187,37 @@ test.describe("Sliding Sync", () => { await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached(); }); + test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => { + // create rooms and check room names are correct + const roomIds: string[] = []; + for (const fruit of ["Apple", "Pineapple", "Orange"]) { + const id = await app.client.createRoom({ name: fruit }); + roomIds.push(id); + await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); + } + const [roomAId, roomPId] = roomIds; + + const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { + if (!request.url().includes("/sync")) return false; + const body = request.postDataJSON(); + return body.room_subscriptions?.[subRoomId]; + }; + + // Select the Test Room and wait for playwright to get the request + const [request] = await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomAId)), + page.getByRole("treeitem", { name: "Apple", exact: true }).click(), + ]); + const roomSubscriptions = request.postDataJSON().room_subscriptions; + expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); + + // Switch to another room and wait for playwright to get the request + await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomPId)), + page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), + ]); + }); + test("should show and be able to accept/reject/rescind invites", async ({ page, app, diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 24124d54747..a7ab59ac844 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -46,6 +46,7 @@ const CONFIG_JSON: Partial = { features: { // We don't want to go through the feature announcement during the e2e test feature_release_announcement: false, + feature_simplified_sliding_sync: false, }, }; From 378b8c95604070bb738921868fdbeee74b6a7107 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 14:42:31 +0000 Subject: [PATCH 35/40] Use UserFriendlyError --- src/MatrixClientPeg.ts | 4 +- src/i18n/strings/en_EN.json | 8195 ++++++++++++++++++----------------- 2 files changed, 4099 insertions(+), 4100 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 3e8ed8c8092..288878cddc5 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -299,9 +299,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { - throw new Error( - "Legacy sliding sync is no longer supported: please log out and back in to enable the new sliding sync flag", - ); + throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported"); } // If the user has enabled the labs feature for sliding sync, set it up diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f8769501f78..262f8fb5f7d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1,4117 +1,4118 @@ { - "a11y": { - "emoji_picker": "Emoji picker", - "jump_first_invite": "Jump to first invite.", - "message_composer": "Message composer", - "n_unread_messages": { - "one": "1 unread message.", - "other": "%(count)s unread messages." - }, - "n_unread_messages_mentions": { - "one": "1 unread mention.", - "other": "%(count)s unread messages including mentions." - }, - "recent_rooms": "Recent rooms", - "room_name": "Room %(name)s", - "room_status_bar": "Room status bar", - "seek_bar_label": "Audio seek bar", - "unread_messages": "Unread messages.", - "user_menu": "User menu" - }, - "a11y_jump_first_unread_room": "Jump to first unread room.", - "action": { - "accept": "Accept", - "add": "Add", - "add_existing_room": "Add existing room", - "add_people": "Add people", - "apply": "Apply", - "approve": "Approve", - "ask_to_join": "Ask to join", - "back": "Back", - "call": "Call", - "cancel": "Cancel", - "change": "Change", - "clear": "Clear", - "click": "Click", - "click_to_copy": "Click to copy", - "close": "Close", - "collapse": "Collapse", - "complete": "Complete", - "confirm": "Confirm", - "continue": "Continue", - "copy": "Copy", - "copy_link": "Copy link", - "create": "Create", - "create_a_room": "Create a room", - "create_account": "Create Account", - "decline": "Decline", - "delete": "Delete", - "deny": "Deny", - "disable": "Disable", - "disconnect": "Disconnect", - "dismiss": "Dismiss", - "done": "Done", - "download": "Download", - "edit": "Edit", - "enable": "Enable", - "enter_fullscreen": "Enter fullscreen", - "exit_fullscreeen": "Exit fullscreen", - "expand": "Expand", - "explore_public_rooms": "Explore public rooms", - "explore_rooms": "Explore rooms", - "export": "Export", - "forward": "Forward", - "go": "Go", - "go_back": "Go back", - "got_it": "Got it", - "hide_advanced": "Hide advanced", - "hold": "Hold", - "ignore": "Ignore", - "import": "Import", - "invite": "Invite", - "invite_to_space": "Invite to space", - "invites_list": "Invites", - "join": "Join", - "learn_more": "Learn more", - "leave": "Leave", - "leave_room": "Leave room", - "logout": "Logout", - "manage": "Manage", - "maximise": "Maximise", - "mention": "Mention", - "minimise": "Minimise", - "new_room": "New room", - "new_video_room": "New video room", - "next": "Next", - "no": "No", - "ok": "OK", - "open": "Open", - "pause": "Pause", - "pin": "Pin", - "play": "Play", - "proceed": "Proceed", - "quote": "Quote", - "react": "React", - "refresh": "Refresh", - "register": "Register", - "reject": "Reject", - "reload": "Reload", - "remove": "Remove", - "rename": "Rename", - "reply": "Reply", - "reply_in_thread": "Reply in thread", - "report_content": "Report Content", - "resend": "Resend", - "reset": "Reset", - "resume": "Resume", - "retry": "Retry", - "review": "Review", - "revoke": "Revoke", - "save": "Save", - "search": "Search", - "send_report": "Send report", - "set_avatar": "Set profile picture", - "share": "Share", - "show": "Show", - "show_advanced": "Show advanced", - "show_all": "Show all", - "sign_in": "Sign in", - "sign_out": "Sign out", - "skip": "Skip", - "start": "Start", - "start_chat": "Start chat", - "start_new_chat": "Start new chat", - "stop": "Stop", - "submit": "Submit", - "subscribe": "Subscribe", - "transfer": "Transfer", - "trust": "Trust", - "try_again": "Try again", - "unban": "Unban", - "unignore": "Unignore", - "unpin": "Unpin", - "unsubscribe": "Unsubscribe", - "update": "Update", - "upgrade": "Upgrade", - "upload": "Upload", - "upload_file": "Upload file", - "verify": "Verify", - "view": "View", - "view_all": "View all", - "view_list": "View list", - "view_message": "View message", - "view_source": "View Source", - "yes": "Yes", - "zoom_in": "Zoom in", - "zoom_out": "Zoom out" - }, - "analytics": { - "accept_button": "That's fine", - "bullet_1": "We don't record or profile any account data", - "bullet_2": "We don't share information with third parties", - "consent_migration": "You previously consented to share anonymous usage data with us. We're updating how that works.", - "disable_prompt": "You can turn this off anytime in settings", - "enable_prompt": "Help improve %(analyticsOwner)s", - "learn_more": "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More", - "privacy_policy": "You can read all our terms here", - "pseudonymous_usage_data": "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.", - "shared_data_heading": "Any of the following data may be shared:" - }, - "auth": { - "3pid_in_use": "That e-mail address or phone number is already in use.", - "account_clash": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", - "account_clash_previous_account": "Continue with previous account", - "account_deactivated": "This account has been deactivated.", - "autodiscovery_generic_failure": "Failed to get autodiscovery configuration from server", - "autodiscovery_hs_incompatible": "Your homeserver is too old and does not support the minimum API version required. Please contact your server owner, or upgrade your server.", - "autodiscovery_invalid": "Invalid homeserver discovery response", - "autodiscovery_invalid_hs": "Homeserver URL does not appear to be a valid Matrix homeserver", - "autodiscovery_invalid_hs_base_url": "Invalid base_url for m.homeserver", - "autodiscovery_invalid_is": "Identity server URL does not appear to be a valid identity server", - "autodiscovery_invalid_is_base_url": "Invalid base_url for m.identity_server", - "autodiscovery_invalid_is_response": "Invalid identity server discovery response", - "autodiscovery_invalid_json": "Invalid JSON", - "autodiscovery_no_well_known": "No .well-known JSON file found", - "autodiscovery_unexpected_error_hs": "Unexpected error resolving homeserver configuration", - "autodiscovery_unexpected_error_is": "Unexpected error resolving identity server configuration", - "captcha_description": "This homeserver would like to make sure you are not a robot.", - "change_password_action": "Change Password", - "change_password_confirm_invalid": "Passwords don't match", - "change_password_confirm_label": "Confirm password", - "change_password_current_label": "Current password", - "change_password_empty": "Passwords can't be empty", - "change_password_error": "Error while changing password: %(error)s", - "change_password_mismatch": "New passwords don't match", - "change_password_new_label": "New Password", - "check_email_explainer": "Follow the instructions sent to %(email)s", - "check_email_resend_prompt": "Did not receive it?", - "check_email_resend_tooltip": "Verification link email resent!", - "check_email_wrong_email_button": "Re-enter email address", - "check_email_wrong_email_prompt": "Wrong email address?", - "continue_with_idp": "Continue with %(provider)s", - "continue_with_sso": "Continue with %(ssoButtons)s", - "country_dropdown": "Country Dropdown", - "create_account_prompt": "New here? Create an account", - "create_account_title": "Create account", - "email_discovery_text": "Use email to optionally be discoverable by existing contacts.", - "email_field_label": "Email", - "email_field_label_invalid": "Doesn't look like a valid email address", - "email_field_label_required": "Enter email address", - "email_help_text": "Add an email to be able to reset your password.", - "email_phone_discovery_text": "Use email or phone to optionally be discoverable by existing contacts.", - "enter_email_explainer": "%(homeserver)s will send you a verification link to let you reset your password.", - "enter_email_heading": "Enter your email to reset password", - "failed_connect_identity_server": "Cannot reach identity server", - "failed_connect_identity_server_other": "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", - "failed_connect_identity_server_register": "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", - "failed_connect_identity_server_reset_password": "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", - "failed_homeserver_discovery": "Failed to perform homeserver discovery", - "failed_query_registration_methods": "Unable to query for supported registration methods.", - "failed_soft_logout_auth": "Failed to re-authenticate", - "failed_soft_logout_homeserver": "Failed to re-authenticate due to a homeserver problem", - "forgot_password_email_invalid": "The email address doesn't appear to be valid.", - "forgot_password_email_required": "The email address linked to your account must be entered.", - "forgot_password_prompt": "Forgotten your password?", - "forgot_password_send_email": "Send email", - "identifier_label": "Sign in with", - "incorrect_credentials": "Incorrect username and/or password.", - "incorrect_credentials_detail": "Please note you are logging into the %(hs)s server, not matrix.org.", - "incorrect_password": "Incorrect password", - "log_in_new_account": "Log in to your new account.", - "logout_dialog": { - "description": "Are you sure you want to sign out?", - "megolm_export": "Manually export keys", - "setup_key_backup_title": "You'll lose access to your encrypted messages", - "setup_secure_backup_description_1": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", - "setup_secure_backup_description_2": "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.", - "skip_key_backup": "I don't want my encrypted messages", - "use_key_backup": "Start using Key Backup" - }, - "misconfigured_body": "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.", - "misconfigured_title": "Your %(brand)s is misconfigured", - "mobile_create_account_title": "You're about to create an account on %(hsName)s", - "msisdn_field_description": "Other users can invite you to rooms using your contact details", - "msisdn_field_label": "Phone", - "msisdn_field_number_invalid": "That phone number doesn't look quite right, please check and try again", - "msisdn_field_required_invalid": "Enter phone number", - "no_hs_url_provided": "No homeserver URL provided", - "oidc": { - "error_title": "We couldn't log you in", - "generic_auth_error": "Something went wrong during authentication. Go to the sign in page and try again.", - "missing_or_invalid_stored_state": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again." - }, - "password_field_keep_going_prompt": "Keep going…", - "password_field_label": "Enter password", - "password_field_strong_label": "Nice, strong password!", - "password_field_weak_label": "Password is allowed, but unsafe", - "phone_label": "Phone", - "phone_optional_label": "Phone (optional)", - "qr_code_login": { - "check_code_explainer": "This will verify that the connection to your other device is secure.", - "check_code_heading": "Enter the number shown on your other device", - "check_code_input_label": "2-digit code", - "check_code_mismatch": "The numbers don't match", - "completing_setup": "Completing set up of your new device", - "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", - "error_expired": "Sign in expired. Please try again.", - "error_expired_title": "The sign in was not completed in time", - "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", - "error_insecure_channel_detected_instructions": "Now what?", - "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", - "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", - "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", - "error_insecure_channel_detected_title": "Connection not secure", - "error_other_device_already_signed_in": "You don’t need to do anything else.", - "error_other_device_already_signed_in_title": "Your other device is already signed in", - "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", - "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", - "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", - "error_unsupported_protocol_title": "Other device not compatible", - "error_user_cancelled": "The sign in was cancelled on the other device.", - "error_user_cancelled_title": "Sign in request cancelled", - "error_user_declined": "You or the account provider declined the sign in request.", - "error_user_declined_title": "Sign in declined", - "follow_remaining_instructions": "Follow the remaining instructions", - "open_element_other_device": "Open %(brand)s on your other device", - "point_the_camera": "Scan the QR code shown here", - "scan_code_instruction": "Scan the QR code with another device", - "scan_qr_code": "Sign in with QR code", - "security_code": "Security code", - "security_code_prompt": "If asked, enter the code below on your other device.", - "select_qr_code": "Select \"%(scanQRCode)s\"", - "unsupported_explainer": "Your account provider doesn't support signing into a new device with a QR code.", - "unsupported_heading": "QR code not supported", - "waiting_for_device": "Waiting for device to sign in" - }, - "register_action": "Create Account", - "registration": { - "continue_without_email_description": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", - "continue_without_email_field_label": "Email (optional)", - "continue_without_email_title": "Continuing without email" - }, - "registration_disabled": "Registration has been disabled on this homeserver.", - "registration_msisdn_field_required_invalid": "Enter phone number (required on this homeserver)", - "registration_successful": "Registration Successful", - "registration_username_in_use": "Someone already has that username. Try another or if it is you, sign in below.", - "registration_username_unable_check": "Unable to check if username has been taken. Try again later.", - "registration_username_validation": "Use lowercase letters, numbers, dashes and underscores only", - "reset_password": { - "confirm_new_password": "Confirm new password", - "devices_logout_success": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", - "other_devices_logout_warning_1": "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", - "other_devices_logout_warning_2": "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", - "password_not_entered": "A new password must be entered.", - "passwords_mismatch": "New passwords must match each other.", - "rate_limit_error": "Too many attempts in a short time. Wait some time before trying again.", - "rate_limit_error_with_time": "Too many attempts in a short time. Retry after %(timeout)s.", - "reset_successful": "Your password has been reset.", - "return_to_login": "Return to login screen", - "sign_out_other_devices": "Sign out of all devices" - }, - "reset_password_action": "Reset password", - "reset_password_button": "Forgot password?", - "reset_password_email_field_description": "Use an email address to recover your account", - "reset_password_email_field_required_invalid": "Enter email address (required on this homeserver)", - "reset_password_email_not_associated": "Your email address does not appear to be associated with a Matrix ID on this homeserver.", - "reset_password_email_not_found_title": "This email address was not found", - "reset_password_title": "Reset your password", - "server_picker_custom": "Other homeserver", - "server_picker_description": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.", - "server_picker_description_matrix.org": "Join millions for free on the largest public server", - "server_picker_dialog_title": "Decide where your account is hosted", - "server_picker_explainer": "Use your preferred Matrix homeserver if you have one, or host your own.", - "server_picker_failed_validate_homeserver": "Unable to validate homeserver", - "server_picker_intro": "We call the places where you can host your account 'homeservers'.", - "server_picker_invalid_url": "Invalid URL", - "server_picker_learn_more": "About homeservers", - "server_picker_matrix.org": "Matrix.org is the biggest public homeserver in the world, so it's a good place for many.", - "server_picker_required": "Specify a homeserver", - "server_picker_title": "Sign into your homeserver", - "server_picker_title_default": "Server Options", - "server_picker_title_registration": "Host account on", - "session_logged_out_description": "For security, this session has been signed out. Please sign in again.", - "session_logged_out_title": "Signed Out", - "set_email": { - "description": "This will allow you to reset your password and receive notifications.", - "verification_pending_description": "Please check your email and click on the link it contains. Once this is done, click continue.", - "verification_pending_title": "Verification Pending" - }, - "set_email_prompt": "Do you want to set an email address?", - "sign_in_description": "Use your account to continue.", - "sign_in_instead": "Sign in instead", - "sign_in_instead_prompt": "Already have an account? Sign in here", - "sign_in_or_register": "Sign In or Create Account", - "sign_in_or_register_description": "Use your account or create a new one to continue.", - "sign_in_prompt": "Got an account? Sign in", - "sign_in_with_sso": "Sign in with single sign-on", - "signing_in": "Signing In…", - "soft_logout": { - "clear_data_button": "Clear all data", - "clear_data_description": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.", - "clear_data_title": "Clear all data in this session?" - }, - "soft_logout_heading": "You're signed out", - "soft_logout_intro_password": "Enter your password to sign in and regain access to your account.", - "soft_logout_intro_sso": "Sign in and regain access to your account.", - "soft_logout_intro_unsupported_auth": "You cannot sign in to your account. Please contact your homeserver admin for more information.", - "soft_logout_subheading": "Clear personal data", - "soft_logout_warning": "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", - "sso": "Single Sign On", - "sso_complete_in_browser_dialog_title": "Go to your browser to complete Sign In", - "sso_failed_missing_storage": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", - "sso_or_username_password": "%(ssoButtons)s Or %(usernamePassword)s", - "sync_footer_subtitle": "If you've joined lots of rooms, this might take a while", - "syncing": "Syncing…", - "uia": { - "code": "Code", - "email": "To create your account, open the link in the email we just sent to %(emailAddress)s.", - "email_auth_header": "Check your email to continue", - "email_resend_prompt": "Did not receive it? Resend it", - "email_resent": "Resent!", - "fallback_button": "Start authentication", - "mas_cross_signing_reset_cta": "Go to your account", - "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", - "msisdn": "A text message has been sent to %(msisdn)s", - "msisdn_token_incorrect": "Token incorrect", - "msisdn_token_prompt": "Please enter the code it contains:", - "password_prompt": "Confirm your identity by entering your account password below.", - "recaptcha_missing_params": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", - "registration_token_label": "Registration token", - "registration_token_prompt": "Enter a registration token provided by the homeserver administrator.", - "sso_body": "Confirm adding this email address by using Single Sign On to prove your identity.", - "sso_failed": "Something went wrong in confirming your identity. Cancel and try again.", - "sso_postauth_body": "Click the button below to confirm your identity.", - "sso_postauth_title": "Confirm to continue", - "sso_preauth_body": "To continue, use Single Sign On to prove your identity.", - "sso_title": "Use Single Sign On to continue", - "terms": "Please review and accept the policies of this homeserver:", - "terms_invalid": "Please review and accept all of the homeserver's policies" - }, - "unsupported_auth": "This homeserver doesn't offer any login flows that are supported by this client.", - "unsupported_auth_email": "This homeserver does not support login using email address.", - "unsupported_auth_msisdn": "This server does not support authentication with a phone number.", - "username_field_required_invalid": "Enter username", - "username_in_use": "Someone already has that username, please try another.", - "verify_email_explainer": "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", - "verify_email_heading": "Verify your email to continue" - }, - "bug_reporting": { - "additional_context": "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.", - "before_submitting": "We recommend creating a GitHub issue to ensure that your report is reviewed.", - "collecting_information": "Collecting app version information", - "collecting_logs": "Collecting logs", - "create_new_issue": "Please create a new issue on GitHub so that we can investigate this bug.", - "description": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.", - "download_logs": "Download logs", - "downloading_logs": "Downloading logs", - "error_empty": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", - "failed_send_logs": "Failed to send logs: ", - "github_issue": "GitHub issue", - "introduction": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ", - "log_request": "To help us prevent this in future, please send us logs.", - "logs_sent": "Logs sent", - "matrix_security_issue": "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.", - "preparing_download": "Preparing to download logs", - "preparing_logs": "Preparing to send logs", - "send_logs": "Send logs", - "submit_debug_logs": "Submit debug logs", - "textarea_label": "Notes", - "thank_you": "Thank you!", - "title": "Bug reporting", - "unsupported_browser": "Reminder: Your browser is unsupported, so your experience may be unpredictable.", - "uploading_logs": "Uploading logs", - "waiting_for_server": "Waiting for response from server" - }, - "cannot_invite_without_identity_server": "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".", - "cannot_reach_homeserver": "Cannot reach homeserver", - "cannot_reach_homeserver_detail": "Ensure you have a stable internet connection, or get in touch with the server admin", - "cant_load_page": "Couldn't load page", - "chat_card_back_action_label": "Back to chat", - "chat_effects": { - "confetti_description": "Sends the given message with confetti", - "confetti_message": "sends confetti", - "fireworks_description": "Sends the given message with fireworks", - "fireworks_message": "sends fireworks", - "hearts_description": "Sends the given message with hearts", - "hearts_message": "sends hearts", - "rainfall_description": "Sends the given message with rainfall", - "rainfall_message": "sends rainfall", - "snowfall_description": "Sends the given message with snowfall", - "snowfall_message": "sends snowfall", - "spaceinvaders_description": "Sends the given message with a space themed effect", - "spaceinvaders_message": "sends space invaders" - }, - "common": { - "access_token": "Access Token", - "accessibility": "Accessibility", - "advanced": "Advanced", - "all_rooms": "All rooms", - "analytics": "Analytics", - "and_n_others": { - "one": "and one other...", - "other": "and %(count)s others..." - }, - "appearance": "Appearance", - "application": "Application", - "are_you_sure": "Are you sure?", - "attachment": "Attachment", - "authentication": "Authentication", - "avatar": "Avatar", - "beta": "Beta", - "camera": "Camera", - "cameras": "Cameras", - "cancel": "Cancel", - "capabilities": "Capabilities", - "copied": "Copied!", - "credits": "Credits", - "cross_signing": "Cross-signing", - "dark": "Dark", - "description": "Description", - "deselect_all": "Deselect all", - "device": "Device", - "edited": "edited", - "email_address": "Email address", - "emoji": "Emoji", - "encrypted": "Encrypted", - "encryption_enabled": "Encryption enabled", - "error": "Error", - "faq": "FAQ", - "favourites": "Favourites", - "feedback": "Feedback", - "filter_results": "Filter results", - "forward_message": "Forward message", - "general": "General", - "go_to_settings": "Go to Settings", - "guest": "Guest", - "help": "Help", - "historical": "Historical", - "home": "Home", - "homeserver": "Homeserver", - "identity_server": "Identity server", - "image": "Image", - "integration_manager": "Integration manager", - "joined": "Joined", - "labs": "Labs", - "legal": "Legal", - "light": "Light", - "loading": "Loading…", - "lobby": "Lobby", - "location": "Location", - "low_priority": "Low priority", - "matrix": "Matrix", - "message": "Message", - "message_layout": "Message layout", - "message_timestamp_invalid": "Invalid timestamp", - "microphone": "Microphone", - "model": "Model", - "modern": "Modern", - "mute": "Mute", - "n_members": { - "one": "%(count)s member", - "other": "%(count)s members" - }, - "n_rooms": { - "one": "%(count)s room", - "other": "%(count)s rooms" - }, - "name": "Name", - "no_results": "No results", - "no_results_found": "No results found", - "not_trusted": "Not trusted", - "off": "Off", - "offline": "Offline", - "on": "On", - "options": "Options", - "orphan_rooms": "Other rooms", - "password": "Password", - "people": "People", - "preferences": "Preferences", - "presence": "Presence", - "preview_message": "Hey you. You're the best!", - "privacy": "Privacy", - "private": "Private", - "private_room": "Private room", - "private_space": "Private space", - "profile": "Profile", - "public": "Public", - "public_room": "Public room", - "public_space": "Public space", - "qr_code": "QR Code", - "random": "Random", - "reactions": "Reactions", - "recommended": "Recommended", - "report_a_bug": "Report a bug", - "room": "Room", - "room_name": "Room name", - "rooms": "Rooms", - "save": "Save", - "saved": "Saved", - "saving": "Saving…", - "secure_backup": "Secure Backup", - "select_all": "Select all", - "server": "Server", - "settings": "Settings", - "setup_secure_messages": "Set up Secure Messages", - "show_more": "Show more", - "someone": "Someone", - "space": "Space", - "spaces": "Spaces", - "sticker": "Sticker", - "stickerpack": "Stickerpack", - "success": "Success", - "suggestions": "Suggestions", - "support": "Support", - "system_alerts": "System Alerts", - "theme": "Theme", - "thread": "Thread", - "threads": "Threads", - "timeline": "Timeline", - "unavailable": "unavailable", - "unencrypted": "Not encrypted", - "unmute": "Unmute", - "unnamed_room": "Unnamed Room", - "unnamed_space": "Unnamed Space", - "unverified": "Unverified", - "updating": "Updating...", - "user": "User", - "user_avatar": "Profile picture", - "username": "Username", - "verification_cancelled": "Verification cancelled", - "verified": "Verified", - "version": "Version", - "video": "Video", - "video_room": "Video room", - "view_message": "View message", - "warning": "Warning" - }, - "composer": { - "autocomplete": { - "@room_description": "Notify the whole room", - "command_a11y": "Command Autocomplete", - "command_description": "Commands", - "emoji_a11y": "Emoji Autocomplete", - "notification_a11y": "Notification Autocomplete", - "notification_description": "Room Notification", - "room_a11y": "Room Autocomplete", - "space_a11y": "Space Autocomplete", - "user_a11y": "User Autocomplete", - "user_description": "Users" - }, - "close_sticker_picker": "Hide stickers", - "edit_composer_label": "Edit message", - "format_bold": "Bold", - "format_code_block": "Code block", - "format_decrease_indent": "Indent decrease", - "format_increase_indent": "Indent increase", - "format_inline_code": "Code", - "format_insert_link": "Insert link", - "format_italic": "Italic", - "format_italics": "Italics", - "format_link": "Link", - "format_ordered_list": "Numbered list", - "format_strikethrough": "Strikethrough", - "format_underline": "Underline", - "format_unordered_list": "Bulleted list", - "formatting_toolbar_label": "Formatting", - "link_modal": { - "link_field_label": "Link", - "text_field_label": "Text", - "title_create": "Create a link", - "title_edit": "Edit link" - }, - "mode_plain": "Hide formatting", - "mode_rich_text": "Show formatting", - "no_perms_notice": "You do not have permission to post to this room", - "placeholder": "Send a message…", - "placeholder_encrypted": "Send an encrypted message…", - "placeholder_reply": "Send a reply…", - "placeholder_reply_encrypted": "Send an encrypted reply…", - "placeholder_thread": "Reply to thread…", - "placeholder_thread_encrypted": "Reply to encrypted thread…", - "poll_button": "Poll", - "poll_button_no_perms_description": "You do not have permission to start polls in this room.", - "poll_button_no_perms_title": "Permission Required", - "replying_title": "Replying", - "room_upgraded_link": "The conversation continues here.", - "room_upgraded_notice": "This room has been replaced and is no longer active.", - "send_button_title": "Send message", - "send_button_voice_message": "Send voice message", - "send_voice_message": "Send voice message", - "stop_voice_message": "Stop recording", - "voice_message_button": "Voice Message" - }, - "console_dev_note": "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!", - "console_scam_warning": "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!", - "console_wait": "Wait!", - "create_room": { - "action_create_room": "Create room", - "action_create_video_room": "Create video room", - "encrypted_video_room_warning": "You can't disable this later. The room will be encrypted but the embedded call will not.", - "encrypted_warning": "You can't disable this later. Bridges & most bots won't work yet.", - "encryption_forced": "Your server requires encryption to be enabled in private rooms.", - "encryption_label": "Enable end-to-end encryption", - "error_title": "Failure to create room", - "generic_error": "Server may be unavailable, overloaded, or you hit a bug.", - "join_rule_change_notice": "You can change this at any time from room settings.", - "join_rule_invite": "Private room (invite only)", - "join_rule_invite_label": "Only people invited will be able to find and join this room.", - "join_rule_knock_label": "Anyone can request to join, but admins or moderators need to grant access. You can change this later.", - "join_rule_public_label": "Anyone will be able to find and join this room.", - "join_rule_public_parent_space_label": "Anyone will be able to find and join this room, not just members of .", - "join_rule_restricted": "Visible to space members", - "join_rule_restricted_label": "Everyone in will be able to find and join this room.", - "name_validation_required": "Please enter a name for the room", - "room_visibility_label": "Room visibility", - "title_private_room": "Create a private room", - "title_public_room": "Create a public room", - "title_video_room": "Create a video room", - "topic_label": "Topic (optional)", - "unfederated": "Block anyone not part of %(serverName)s from ever joining this room.", - "unfederated_label_default_off": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", - "unfederated_label_default_on": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", - "unsupported_version": "The server does not support the room version specified." - }, - "create_space": { - "add_details_prompt": "Add some details to help people recognise it.", - "add_details_prompt_2": "You can change these anytime.", - "add_existing_rooms_description": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", - "add_existing_rooms_heading": "What do you want to organise?", - "address_label": "Address", - "address_placeholder": "e.g. my-space", - "creating": "Creating…", - "creating_rooms": "Creating rooms…", - "done_action": "Go to my space", - "done_action_first_room": "Go to my first room", - "explainer": "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.", - "failed_create_initial_rooms": "Failed to create initial space rooms", - "failed_invite_users": "Failed to invite the following users to your space: %(csvUsers)s", - "invite_teammates_by_username": "Invite by username", - "invite_teammates_description": "Make sure the right people have access. You can invite more later.", - "invite_teammates_heading": "Invite your teammates", - "inviting_users": "Inviting…", - "label": "Create a space", - "name_required": "Please enter a name for the space", - "personal_space": "Just me", - "personal_space_description": "A private space to organise your rooms", - "private_description": "Invite only, best for yourself or teams", - "private_heading": "Your private space", - "private_personal_description": "Make sure the right people have access to %(name)s", - "private_personal_heading": "Who are you working with?", - "private_space": "Me and my teammates", - "private_space_description": "A private space for you and your teammates", - "public_description": "Open space for anyone, best for communities", - "public_heading": "Your public space", - "search_public_button": "Search for public spaces", - "setup_rooms_community_description": "Let's create a room for each of them.", - "setup_rooms_community_heading": "What are some things you want to discuss in %(spaceName)s?", - "setup_rooms_description": "You can add more later too, including already existing ones.", - "setup_rooms_private_description": "We'll create rooms for each of them.", - "setup_rooms_private_heading": "What projects are your team working on?", - "share_description": "It's just you at the moment, it will be even better with others.", - "share_heading": "Share %(name)s", - "skip_action": "Skip for now", - "subspace_adding": "Adding…", - "subspace_beta_notice": "Add a space to a space you manage.", - "subspace_dropdown_title": "Create a space", - "subspace_existing_space_prompt": "Want to add an existing space instead?", - "subspace_join_rule_invite_description": "Only people invited will be able to find and join this space.", - "subspace_join_rule_invite_only": "Private space (invite only)", - "subspace_join_rule_label": "Space visibility", - "subspace_join_rule_public_description": "Anyone will be able to find and join this space, not just members of .", - "subspace_join_rule_restricted_description": "Anyone in will be able to find and join." - }, - "credits": { - "default_cover_photo": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.", - "twemoji": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.", - "twemoji_colr": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0." - }, - "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", - "devtools": { - "active_widgets": "Active Widgets", - "category_other": "Other", - "category_room": "Room", - "caution_colon": "Caution:", - "client_versions": "Client Versions", - "crypto": { - "4s_public_key_in_account_data": "in account data", - "4s_public_key_not_in_account_data": "not found", - "4s_public_key_status": "Secret storage public key:", - "backup_key_cached": "cached locally", - "backup_key_cached_status": "Backup key cached:", - "backup_key_not_stored": "not stored", - "backup_key_stored": "in secret storage", - "backup_key_stored_status": "Backup key stored:", - "backup_key_unexpected_type": "unexpected type", - "backup_key_well_formed": "well formed", - "cross_signing": "Cross-signing", - "cross_signing_cached": "cached locally", - "cross_signing_not_ready": "Cross-signing is not set up.", - "cross_signing_private_keys_in_storage": "in secret storage", - "cross_signing_private_keys_in_storage_status": "Cross-signing private keys:", - "cross_signing_private_keys_not_in_storage": "not found in storage", - "cross_signing_public_keys_on_device": "in memory", - "cross_signing_public_keys_on_device_status": "Cross-signing public keys:", - "cross_signing_ready": "Cross-signing is ready for use.", - "cross_signing_status": "Cross-signing status:", - "cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", - "crypto_not_available": "Cryptographic module is not available", - "key_backup_active_version": "Active backup version:", - "key_backup_active_version_none": "None", - "key_backup_inactive_warning": "Your keys are not being backed up from this session.", - "key_backup_latest_version": "Latest backup version on server:", - "key_storage": "Key Storage", - "master_private_key_cached_status": "Master private key:", - "not_found": "not found", - "not_found_locally": "not found locally", - "secret_storage_not_ready": "not ready", - "secret_storage_ready": "ready", - "secret_storage_status": "Secret storage:", - "self_signing_private_key_cached_status": "Self signing private key:", - "title": "End-to-end encryption", - "user_signing_private_key_cached_status": "User signing private key:" - }, - "developer_mode": "Developer mode", - "developer_tools": "Developer Tools", - "edit_setting": "Edit setting", - "edit_values": "Edit values", - "empty_string": "", - "event_content": "Event Content", - "event_id": "Event ID: %(eventId)s", - "event_sent": "Event sent!", - "event_type": "Event Type", - "explore_account_data": "Explore account data", - "explore_room_account_data": "Explore room account data", - "explore_room_state": "Explore room state", - "failed_to_find_widget": "There was an error finding this widget.", - "failed_to_load": "Failed to load.", - "failed_to_save": "Failed to save settings.", - "failed_to_send": "Failed to send event!", - "id": "ID: ", - "invalid_json": "Doesn't look like valid JSON.", - "level": "Level", - "low_bandwidth_mode": "Low bandwidth mode", - "low_bandwidth_mode_description": "Requires compatible homeserver.", - "main_timeline": "Main timeline", - "no_receipt_found": "No receipt found", - "notification_state": "Notification state is %(notificationState)s", - "notifications_debug": "Notifications debug", - "number_of_users": "Number of users", - "original_event_source": "Original event source", - "room_encrypted": "Room is encrypted ✅", - "room_id": "Room ID: %(roomId)s", - "room_not_encrypted": "Room is not encrypted 🚨", - "room_notifications_dot": "Dot: ", - "room_notifications_highlight": "Highlight: ", - "room_notifications_last_event": "Last event:", - "room_notifications_sender": "Sender: ", - "room_notifications_thread_id": "Thread Id: ", - "room_notifications_total": "Total: ", - "room_notifications_type": "Type: ", - "room_status": "Room status", - "room_unread_status_count": { - "one": "Room unread status: %(status)s, count: %(count)s", - "other": "Room unread status: %(status)s, count: %(count)s" - }, - "save_setting_values": "Save setting values", - "see_history": "See history", - "send_custom_account_data_event": "Send custom account data event", - "send_custom_room_account_data_event": "Send custom room account data event", - "send_custom_state_event": "Send custom state event", - "send_custom_timeline_event": "Send custom timeline event", - "server_info": "Server info", - "server_versions": "Server Versions", - "settable_global": "Settable at global", - "settable_room": "Settable at room", - "setting_colon": "Setting:", - "setting_definition": "Setting definition:", - "setting_id": "Setting ID", - "settings_explorer": "Settings explorer", - "show_hidden_events": "Show hidden events in timeline", - "spaces": { - "one": "", - "other": "<%(count)s spaces>" - }, - "state_key": "State Key", - "thread_root_id": "Thread Root ID: %(threadRootId)s", - "threads_timeline": "Threads timeline", - "title": "Developer tools", - "toggle_event": "toggle event", - "toolbox": "Toolbox", - "use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.", - "user_read_up_to": "User read up to: ", - "user_read_up_to_ignore_synthetic": "User read up to (ignoreSynthetic): ", - "user_read_up_to_private": "User read up to (m.read.private): ", - "user_read_up_to_private_ignore_synthetic": "User read up to (m.read.private;ignoreSynthetic): ", - "value": "Value", - "value_colon": "Value:", - "value_in_this_room": "Value in this room", - "value_this_room_colon": "Value in this room:", - "values_explicit": "Values at explicit levels", - "values_explicit_colon": "Values at explicit levels:", - "values_explicit_room": "Values at explicit levels in this room", - "values_explicit_this_room_colon": "Values at explicit levels in this room:", - "view_servers_in_room": "View servers in room", - "view_source_decrypted_event_source": "Decrypted event source", - "view_source_decrypted_event_source_unavailable": "Decrypted source unavailable", - "widget_screenshots": "Enable widget screenshots on supported widgets" - }, - "dialog_close_label": "Close dialog", - "download_completed": "Download Completed", - "emoji": { - "categories": "Categories", - "category_activities": "Activities", - "category_animals_nature": "Animals & Nature", - "category_flags": "Flags", - "category_food_drink": "Food & Drink", - "category_frequently_used": "Frequently Used", - "category_objects": "Objects", - "category_smileys_people": "Smileys & People", - "category_symbols": "Symbols", - "category_travel_places": "Travel & Places", - "quick_reactions": "Quick Reactions" - }, - "emoji_picker": { - "cancel_search_label": "Cancel search" - }, - "empty_room": "Empty room", - "empty_room_was_name": "Empty room (was %(oldName)s)", + "a11y": { + "emoji_picker": "Emoji picker", + "jump_first_invite": "Jump to first invite.", + "message_composer": "Message composer", + "n_unread_messages": { + "one": "1 unread message.", + "other": "%(count)s unread messages." + }, + "n_unread_messages_mentions": { + "one": "1 unread mention.", + "other": "%(count)s unread messages including mentions." + }, + "recent_rooms": "Recent rooms", + "room_name": "Room %(name)s", + "room_status_bar": "Room status bar", + "seek_bar_label": "Audio seek bar", + "unread_messages": "Unread messages.", + "user_menu": "User menu" + }, + "a11y_jump_first_unread_room": "Jump to first unread room.", + "action": { + "accept": "Accept", + "add": "Add", + "add_existing_room": "Add existing room", + "add_people": "Add people", + "apply": "Apply", + "approve": "Approve", + "ask_to_join": "Ask to join", + "back": "Back", + "call": "Call", + "cancel": "Cancel", + "change": "Change", + "clear": "Clear", + "click": "Click", + "click_to_copy": "Click to copy", + "close": "Close", + "collapse": "Collapse", + "complete": "Complete", + "confirm": "Confirm", + "continue": "Continue", + "copy": "Copy", + "copy_link": "Copy link", + "create": "Create", + "create_a_room": "Create a room", + "create_account": "Create Account", + "decline": "Decline", + "delete": "Delete", + "deny": "Deny", + "disable": "Disable", + "disconnect": "Disconnect", + "dismiss": "Dismiss", + "done": "Done", + "download": "Download", + "edit": "Edit", + "enable": "Enable", + "enter_fullscreen": "Enter fullscreen", + "exit_fullscreeen": "Exit fullscreen", + "expand": "Expand", + "explore_public_rooms": "Explore public rooms", + "explore_rooms": "Explore rooms", + "export": "Export", + "forward": "Forward", + "go": "Go", + "go_back": "Go back", + "got_it": "Got it", + "hide_advanced": "Hide advanced", + "hold": "Hold", + "ignore": "Ignore", + "import": "Import", + "invite": "Invite", + "invite_to_space": "Invite to space", + "invites_list": "Invites", + "join": "Join", + "learn_more": "Learn more", + "leave": "Leave", + "leave_room": "Leave room", + "logout": "Logout", + "manage": "Manage", + "maximise": "Maximise", + "mention": "Mention", + "minimise": "Minimise", + "new_room": "New room", + "new_video_room": "New video room", + "next": "Next", + "no": "No", + "ok": "OK", + "open": "Open", + "pause": "Pause", + "pin": "Pin", + "play": "Play", + "proceed": "Proceed", + "quote": "Quote", + "react": "React", + "refresh": "Refresh", + "register": "Register", + "reject": "Reject", + "reload": "Reload", + "remove": "Remove", + "rename": "Rename", + "reply": "Reply", + "reply_in_thread": "Reply in thread", + "report_content": "Report Content", + "resend": "Resend", + "reset": "Reset", + "resume": "Resume", + "retry": "Retry", + "review": "Review", + "revoke": "Revoke", + "save": "Save", + "search": "Search", + "send_report": "Send report", + "set_avatar": "Set profile picture", + "share": "Share", + "show": "Show", + "show_advanced": "Show advanced", + "show_all": "Show all", + "sign_in": "Sign in", + "sign_out": "Sign out", + "skip": "Skip", + "start": "Start", + "start_chat": "Start chat", + "start_new_chat": "Start new chat", + "stop": "Stop", + "submit": "Submit", + "subscribe": "Subscribe", + "transfer": "Transfer", + "trust": "Trust", + "try_again": "Try again", + "unban": "Unban", + "unignore": "Unignore", + "unpin": "Unpin", + "unsubscribe": "Unsubscribe", + "update": "Update", + "upgrade": "Upgrade", + "upload": "Upload", + "upload_file": "Upload file", + "verify": "Verify", + "view": "View", + "view_all": "View all", + "view_list": "View list", + "view_message": "View message", + "view_source": "View Source", + "yes": "Yes", + "zoom_in": "Zoom in", + "zoom_out": "Zoom out" + }, + "analytics": { + "accept_button": "That's fine", + "bullet_1": "We don't record or profile any account data", + "bullet_2": "We don't share information with third parties", + "consent_migration": "You previously consented to share anonymous usage data with us. We're updating how that works.", + "disable_prompt": "You can turn this off anytime in settings", + "enable_prompt": "Help improve %(analyticsOwner)s", + "learn_more": "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More", + "privacy_policy": "You can read all our terms here", + "pseudonymous_usage_data": "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.", + "shared_data_heading": "Any of the following data may be shared:" + }, + "auth": { + "3pid_in_use": "That e-mail address or phone number is already in use.", + "account_clash": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", + "account_clash_previous_account": "Continue with previous account", + "account_deactivated": "This account has been deactivated.", + "autodiscovery_generic_failure": "Failed to get autodiscovery configuration from server", + "autodiscovery_hs_incompatible": "Your homeserver is too old and does not support the minimum API version required. Please contact your server owner, or upgrade your server.", + "autodiscovery_invalid": "Invalid homeserver discovery response", + "autodiscovery_invalid_hs": "Homeserver URL does not appear to be a valid Matrix homeserver", + "autodiscovery_invalid_hs_base_url": "Invalid base_url for m.homeserver", + "autodiscovery_invalid_is": "Identity server URL does not appear to be a valid identity server", + "autodiscovery_invalid_is_base_url": "Invalid base_url for m.identity_server", + "autodiscovery_invalid_is_response": "Invalid identity server discovery response", + "autodiscovery_invalid_json": "Invalid JSON", + "autodiscovery_no_well_known": "No .well-known JSON file found", + "autodiscovery_unexpected_error_hs": "Unexpected error resolving homeserver configuration", + "autodiscovery_unexpected_error_is": "Unexpected error resolving identity server configuration", + "captcha_description": "This homeserver would like to make sure you are not a robot.", + "change_password_action": "Change Password", + "change_password_confirm_invalid": "Passwords don't match", + "change_password_confirm_label": "Confirm password", + "change_password_current_label": "Current password", + "change_password_empty": "Passwords can't be empty", + "change_password_error": "Error while changing password: %(error)s", + "change_password_mismatch": "New passwords don't match", + "change_password_new_label": "New Password", + "check_email_explainer": "Follow the instructions sent to %(email)s", + "check_email_resend_prompt": "Did not receive it?", + "check_email_resend_tooltip": "Verification link email resent!", + "check_email_wrong_email_button": "Re-enter email address", + "check_email_wrong_email_prompt": "Wrong email address?", + "continue_with_idp": "Continue with %(provider)s", + "continue_with_sso": "Continue with %(ssoButtons)s", + "country_dropdown": "Country Dropdown", + "create_account_prompt": "New here? Create an account", + "create_account_title": "Create account", + "email_discovery_text": "Use email to optionally be discoverable by existing contacts.", + "email_field_label": "Email", + "email_field_label_invalid": "Doesn't look like a valid email address", + "email_field_label_required": "Enter email address", + "email_help_text": "Add an email to be able to reset your password.", + "email_phone_discovery_text": "Use email or phone to optionally be discoverable by existing contacts.", + "enter_email_explainer": "%(homeserver)s will send you a verification link to let you reset your password.", + "enter_email_heading": "Enter your email to reset password", + "failed_connect_identity_server": "Cannot reach identity server", + "failed_connect_identity_server_other": "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", + "failed_connect_identity_server_register": "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", + "failed_connect_identity_server_reset_password": "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.", + "failed_homeserver_discovery": "Failed to perform homeserver discovery", + "failed_query_registration_methods": "Unable to query for supported registration methods.", + "failed_soft_logout_auth": "Failed to re-authenticate", + "failed_soft_logout_homeserver": "Failed to re-authenticate due to a homeserver problem", + "forgot_password_email_invalid": "The email address doesn't appear to be valid.", + "forgot_password_email_required": "The email address linked to your account must be entered.", + "forgot_password_prompt": "Forgotten your password?", + "forgot_password_send_email": "Send email", + "identifier_label": "Sign in with", + "incorrect_credentials": "Incorrect username and/or password.", + "incorrect_credentials_detail": "Please note you are logging into the %(hs)s server, not matrix.org.", + "incorrect_password": "Incorrect password", + "log_in_new_account": "Log in to your new account.", + "logout_dialog": { + "description": "Are you sure you want to sign out?", + "megolm_export": "Manually export keys", + "setup_key_backup_title": "You'll lose access to your encrypted messages", + "setup_secure_backup_description_1": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", + "setup_secure_backup_description_2": "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.", + "skip_key_backup": "I don't want my encrypted messages", + "use_key_backup": "Start using Key Backup" + }, + "misconfigured_body": "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.", + "misconfigured_title": "Your %(brand)s is misconfigured", + "mobile_create_account_title": "You're about to create an account on %(hsName)s", + "msisdn_field_description": "Other users can invite you to rooms using your contact details", + "msisdn_field_label": "Phone", + "msisdn_field_number_invalid": "That phone number doesn't look quite right, please check and try again", + "msisdn_field_required_invalid": "Enter phone number", + "no_hs_url_provided": "No homeserver URL provided", + "oidc": { + "error_title": "We couldn't log you in", + "generic_auth_error": "Something went wrong during authentication. Go to the sign in page and try again.", + "missing_or_invalid_stored_state": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again." + }, + "password_field_keep_going_prompt": "Keep going…", + "password_field_label": "Enter password", + "password_field_strong_label": "Nice, strong password!", + "password_field_weak_label": "Password is allowed, but unsafe", + "phone_label": "Phone", + "phone_optional_label": "Phone (optional)", + "qr_code_login": { + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", + "completing_setup": "Completing set up of your new device", + "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", + "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", + "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", + "error_user_declined": "You or the account provider declined the sign in request.", + "error_user_declined_title": "Sign in declined", + "follow_remaining_instructions": "Follow the remaining instructions", + "open_element_other_device": "Open %(brand)s on your other device", + "point_the_camera": "Scan the QR code shown here", + "scan_code_instruction": "Scan the QR code with another device", + "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", + "select_qr_code": "Select \"%(scanQRCode)s\"", + "unsupported_explainer": "Your account provider doesn't support signing into a new device with a QR code.", + "unsupported_heading": "QR code not supported", + "waiting_for_device": "Waiting for device to sign in" + }, + "register_action": "Create Account", + "registration": { + "continue_without_email_description": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", + "continue_without_email_field_label": "Email (optional)", + "continue_without_email_title": "Continuing without email" + }, + "registration_disabled": "Registration has been disabled on this homeserver.", + "registration_msisdn_field_required_invalid": "Enter phone number (required on this homeserver)", + "registration_successful": "Registration Successful", + "registration_username_in_use": "Someone already has that username. Try another or if it is you, sign in below.", + "registration_username_unable_check": "Unable to check if username has been taken. Try again later.", + "registration_username_validation": "Use lowercase letters, numbers, dashes and underscores only", + "reset_password": { + "confirm_new_password": "Confirm new password", + "devices_logout_success": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", + "other_devices_logout_warning_1": "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", + "other_devices_logout_warning_2": "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", + "password_not_entered": "A new password must be entered.", + "passwords_mismatch": "New passwords must match each other.", + "rate_limit_error": "Too many attempts in a short time. Wait some time before trying again.", + "rate_limit_error_with_time": "Too many attempts in a short time. Retry after %(timeout)s.", + "reset_successful": "Your password has been reset.", + "return_to_login": "Return to login screen", + "sign_out_other_devices": "Sign out of all devices" + }, + "reset_password_action": "Reset password", + "reset_password_button": "Forgot password?", + "reset_password_email_field_description": "Use an email address to recover your account", + "reset_password_email_field_required_invalid": "Enter email address (required on this homeserver)", + "reset_password_email_not_associated": "Your email address does not appear to be associated with a Matrix ID on this homeserver.", + "reset_password_email_not_found_title": "This email address was not found", + "reset_password_title": "Reset your password", + "server_picker_custom": "Other homeserver", + "server_picker_description": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.", + "server_picker_description_matrix.org": "Join millions for free on the largest public server", + "server_picker_dialog_title": "Decide where your account is hosted", + "server_picker_explainer": "Use your preferred Matrix homeserver if you have one, or host your own.", + "server_picker_failed_validate_homeserver": "Unable to validate homeserver", + "server_picker_intro": "We call the places where you can host your account 'homeservers'.", + "server_picker_invalid_url": "Invalid URL", + "server_picker_learn_more": "About homeservers", + "server_picker_matrix.org": "Matrix.org is the biggest public homeserver in the world, so it's a good place for many.", + "server_picker_required": "Specify a homeserver", + "server_picker_title": "Sign into your homeserver", + "server_picker_title_default": "Server Options", + "server_picker_title_registration": "Host account on", + "session_logged_out_description": "For security, this session has been signed out. Please sign in again.", + "session_logged_out_title": "Signed Out", + "set_email": { + "description": "This will allow you to reset your password and receive notifications.", + "verification_pending_description": "Please check your email and click on the link it contains. Once this is done, click continue.", + "verification_pending_title": "Verification Pending" + }, + "set_email_prompt": "Do you want to set an email address?", + "sign_in_description": "Use your account to continue.", + "sign_in_instead": "Sign in instead", + "sign_in_instead_prompt": "Already have an account? Sign in here", + "sign_in_or_register": "Sign In or Create Account", + "sign_in_or_register_description": "Use your account or create a new one to continue.", + "sign_in_prompt": "Got an account? Sign in", + "sign_in_with_sso": "Sign in with single sign-on", + "signing_in": "Signing In…", + "soft_logout": { + "clear_data_button": "Clear all data", + "clear_data_description": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.", + "clear_data_title": "Clear all data in this session?" + }, + "soft_logout_heading": "You're signed out", + "soft_logout_intro_password": "Enter your password to sign in and regain access to your account.", + "soft_logout_intro_sso": "Sign in and regain access to your account.", + "soft_logout_intro_unsupported_auth": "You cannot sign in to your account. Please contact your homeserver admin for more information.", + "soft_logout_subheading": "Clear personal data", + "soft_logout_warning": "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", + "sso": "Single Sign On", + "sso_complete_in_browser_dialog_title": "Go to your browser to complete Sign In", + "sso_failed_missing_storage": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", + "sso_or_username_password": "%(ssoButtons)s Or %(usernamePassword)s", + "sync_footer_subtitle": "If you've joined lots of rooms, this might take a while", + "syncing": "Syncing…", + "uia": { + "code": "Code", + "email": "To create your account, open the link in the email we just sent to %(emailAddress)s.", + "email_auth_header": "Check your email to continue", + "email_resend_prompt": "Did not receive it? Resend it", + "email_resent": "Resent!", + "fallback_button": "Start authentication", + "mas_cross_signing_reset_cta": "Go to your account", + "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", + "msisdn": "A text message has been sent to %(msisdn)s", + "msisdn_token_incorrect": "Token incorrect", + "msisdn_token_prompt": "Please enter the code it contains:", + "password_prompt": "Confirm your identity by entering your account password below.", + "recaptcha_missing_params": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", + "registration_token_label": "Registration token", + "registration_token_prompt": "Enter a registration token provided by the homeserver administrator.", + "sso_body": "Confirm adding this email address by using Single Sign On to prove your identity.", + "sso_failed": "Something went wrong in confirming your identity. Cancel and try again.", + "sso_postauth_body": "Click the button below to confirm your identity.", + "sso_postauth_title": "Confirm to continue", + "sso_preauth_body": "To continue, use Single Sign On to prove your identity.", + "sso_title": "Use Single Sign On to continue", + "terms": "Please review and accept the policies of this homeserver:", + "terms_invalid": "Please review and accept all of the homeserver's policies" + }, + "unsupported_auth": "This homeserver doesn't offer any login flows that are supported by this client.", + "unsupported_auth_email": "This homeserver does not support login using email address.", + "unsupported_auth_msisdn": "This server does not support authentication with a phone number.", + "username_field_required_invalid": "Enter username", + "username_in_use": "Someone already has that username, please try another.", + "verify_email_explainer": "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", + "verify_email_heading": "Verify your email to continue" + }, + "bug_reporting": { + "additional_context": "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.", + "before_submitting": "We recommend creating a GitHub issue to ensure that your report is reviewed.", + "collecting_information": "Collecting app version information", + "collecting_logs": "Collecting logs", + "create_new_issue": "Please create a new issue on GitHub so that we can investigate this bug.", + "description": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.", + "download_logs": "Download logs", + "downloading_logs": "Downloading logs", + "error_empty": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", + "failed_send_logs": "Failed to send logs: ", + "github_issue": "GitHub issue", + "introduction": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ", + "log_request": "To help us prevent this in future, please send us logs.", + "logs_sent": "Logs sent", + "matrix_security_issue": "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.", + "preparing_download": "Preparing to download logs", + "preparing_logs": "Preparing to send logs", + "send_logs": "Send logs", + "submit_debug_logs": "Submit debug logs", + "textarea_label": "Notes", + "thank_you": "Thank you!", + "title": "Bug reporting", + "unsupported_browser": "Reminder: Your browser is unsupported, so your experience may be unpredictable.", + "uploading_logs": "Uploading logs", + "waiting_for_server": "Waiting for response from server" + }, + "cannot_invite_without_identity_server": "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".", + "cannot_reach_homeserver": "Cannot reach homeserver", + "cannot_reach_homeserver_detail": "Ensure you have a stable internet connection, or get in touch with the server admin", + "cant_load_page": "Couldn't load page", + "chat_card_back_action_label": "Back to chat", + "chat_effects": { + "confetti_description": "Sends the given message with confetti", + "confetti_message": "sends confetti", + "fireworks_description": "Sends the given message with fireworks", + "fireworks_message": "sends fireworks", + "hearts_description": "Sends the given message with hearts", + "hearts_message": "sends hearts", + "rainfall_description": "Sends the given message with rainfall", + "rainfall_message": "sends rainfall", + "snowfall_description": "Sends the given message with snowfall", + "snowfall_message": "sends snowfall", + "spaceinvaders_description": "Sends the given message with a space themed effect", + "spaceinvaders_message": "sends space invaders" + }, + "common": { + "access_token": "Access Token", + "accessibility": "Accessibility", + "advanced": "Advanced", + "all_rooms": "All rooms", + "analytics": "Analytics", + "and_n_others": { + "one": "and one other...", + "other": "and %(count)s others..." + }, + "appearance": "Appearance", + "application": "Application", + "are_you_sure": "Are you sure?", + "attachment": "Attachment", + "authentication": "Authentication", + "avatar": "Avatar", + "beta": "Beta", + "camera": "Camera", + "cameras": "Cameras", + "cancel": "Cancel", + "capabilities": "Capabilities", + "copied": "Copied!", + "credits": "Credits", + "cross_signing": "Cross-signing", + "dark": "Dark", + "description": "Description", + "deselect_all": "Deselect all", + "device": "Device", + "edited": "edited", + "email_address": "Email address", + "emoji": "Emoji", + "encrypted": "Encrypted", + "encryption_enabled": "Encryption enabled", + "error": "Error", + "faq": "FAQ", + "favourites": "Favourites", + "feedback": "Feedback", + "filter_results": "Filter results", + "forward_message": "Forward message", + "general": "General", + "go_to_settings": "Go to Settings", + "guest": "Guest", + "help": "Help", + "historical": "Historical", + "home": "Home", + "homeserver": "Homeserver", + "identity_server": "Identity server", + "image": "Image", + "integration_manager": "Integration manager", + "joined": "Joined", + "labs": "Labs", + "legal": "Legal", + "light": "Light", + "loading": "Loading…", + "lobby": "Lobby", + "location": "Location", + "low_priority": "Low priority", + "matrix": "Matrix", + "message": "Message", + "message_layout": "Message layout", + "message_timestamp_invalid": "Invalid timestamp", + "microphone": "Microphone", + "model": "Model", + "modern": "Modern", + "mute": "Mute", + "n_members": { + "one": "%(count)s member", + "other": "%(count)s members" + }, + "n_rooms": { + "one": "%(count)s room", + "other": "%(count)s rooms" + }, + "name": "Name", + "no_results": "No results", + "no_results_found": "No results found", + "not_trusted": "Not trusted", + "off": "Off", + "offline": "Offline", + "on": "On", + "options": "Options", + "orphan_rooms": "Other rooms", + "password": "Password", + "people": "People", + "preferences": "Preferences", + "presence": "Presence", + "preview_message": "Hey you. You're the best!", + "privacy": "Privacy", + "private": "Private", + "private_room": "Private room", + "private_space": "Private space", + "profile": "Profile", + "public": "Public", + "public_room": "Public room", + "public_space": "Public space", + "qr_code": "QR Code", + "random": "Random", + "reactions": "Reactions", + "recommended": "Recommended", + "report_a_bug": "Report a bug", + "room": "Room", + "room_name": "Room name", + "rooms": "Rooms", + "save": "Save", + "saved": "Saved", + "saving": "Saving…", + "secure_backup": "Secure Backup", + "select_all": "Select all", + "server": "Server", + "settings": "Settings", + "setup_secure_messages": "Set up Secure Messages", + "show_more": "Show more", + "someone": "Someone", + "space": "Space", + "spaces": "Spaces", + "sticker": "Sticker", + "stickerpack": "Stickerpack", + "success": "Success", + "suggestions": "Suggestions", + "support": "Support", + "system_alerts": "System Alerts", + "theme": "Theme", + "thread": "Thread", + "threads": "Threads", + "timeline": "Timeline", + "unavailable": "unavailable", + "unencrypted": "Not encrypted", + "unmute": "Unmute", + "unnamed_room": "Unnamed Room", + "unnamed_space": "Unnamed Space", + "unverified": "Unverified", + "updating": "Updating...", + "user": "User", + "user_avatar": "Profile picture", + "username": "Username", + "verification_cancelled": "Verification cancelled", + "verified": "Verified", + "version": "Version", + "video": "Video", + "video_room": "Video room", + "view_message": "View message", + "warning": "Warning" + }, + "composer": { + "autocomplete": { + "@room_description": "Notify the whole room", + "command_a11y": "Command Autocomplete", + "command_description": "Commands", + "emoji_a11y": "Emoji Autocomplete", + "notification_a11y": "Notification Autocomplete", + "notification_description": "Room Notification", + "room_a11y": "Room Autocomplete", + "space_a11y": "Space Autocomplete", + "user_a11y": "User Autocomplete", + "user_description": "Users" + }, + "close_sticker_picker": "Hide stickers", + "edit_composer_label": "Edit message", + "format_bold": "Bold", + "format_code_block": "Code block", + "format_decrease_indent": "Indent decrease", + "format_increase_indent": "Indent increase", + "format_inline_code": "Code", + "format_insert_link": "Insert link", + "format_italic": "Italic", + "format_italics": "Italics", + "format_link": "Link", + "format_ordered_list": "Numbered list", + "format_strikethrough": "Strikethrough", + "format_underline": "Underline", + "format_unordered_list": "Bulleted list", + "formatting_toolbar_label": "Formatting", + "link_modal": { + "link_field_label": "Link", + "text_field_label": "Text", + "title_create": "Create a link", + "title_edit": "Edit link" + }, + "mode_plain": "Hide formatting", + "mode_rich_text": "Show formatting", + "no_perms_notice": "You do not have permission to post to this room", + "placeholder": "Send a message…", + "placeholder_encrypted": "Send an encrypted message…", + "placeholder_reply": "Send a reply…", + "placeholder_reply_encrypted": "Send an encrypted reply…", + "placeholder_thread": "Reply to thread…", + "placeholder_thread_encrypted": "Reply to encrypted thread…", + "poll_button": "Poll", + "poll_button_no_perms_description": "You do not have permission to start polls in this room.", + "poll_button_no_perms_title": "Permission Required", + "replying_title": "Replying", + "room_upgraded_link": "The conversation continues here.", + "room_upgraded_notice": "This room has been replaced and is no longer active.", + "send_button_title": "Send message", + "send_button_voice_message": "Send voice message", + "send_voice_message": "Send voice message", + "stop_voice_message": "Stop recording", + "voice_message_button": "Voice Message" + }, + "console_dev_note": "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!", + "console_scam_warning": "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!", + "console_wait": "Wait!", + "create_room": { + "action_create_room": "Create room", + "action_create_video_room": "Create video room", + "encrypted_video_room_warning": "You can't disable this later. The room will be encrypted but the embedded call will not.", + "encrypted_warning": "You can't disable this later. Bridges & most bots won't work yet.", + "encryption_forced": "Your server requires encryption to be enabled in private rooms.", + "encryption_label": "Enable end-to-end encryption", + "error_title": "Failure to create room", + "generic_error": "Server may be unavailable, overloaded, or you hit a bug.", + "join_rule_change_notice": "You can change this at any time from room settings.", + "join_rule_invite": "Private room (invite only)", + "join_rule_invite_label": "Only people invited will be able to find and join this room.", + "join_rule_knock_label": "Anyone can request to join, but admins or moderators need to grant access. You can change this later.", + "join_rule_public_label": "Anyone will be able to find and join this room.", + "join_rule_public_parent_space_label": "Anyone will be able to find and join this room, not just members of .", + "join_rule_restricted": "Visible to space members", + "join_rule_restricted_label": "Everyone in will be able to find and join this room.", + "name_validation_required": "Please enter a name for the room", + "room_visibility_label": "Room visibility", + "title_private_room": "Create a private room", + "title_public_room": "Create a public room", + "title_video_room": "Create a video room", + "topic_label": "Topic (optional)", + "unfederated": "Block anyone not part of %(serverName)s from ever joining this room.", + "unfederated_label_default_off": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", + "unfederated_label_default_on": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", + "unsupported_version": "The server does not support the room version specified." + }, + "create_space": { + "add_details_prompt": "Add some details to help people recognise it.", + "add_details_prompt_2": "You can change these anytime.", + "add_existing_rooms_description": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", + "add_existing_rooms_heading": "What do you want to organise?", + "address_label": "Address", + "address_placeholder": "e.g. my-space", + "creating": "Creating…", + "creating_rooms": "Creating rooms…", + "done_action": "Go to my space", + "done_action_first_room": "Go to my first room", + "explainer": "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.", + "failed_create_initial_rooms": "Failed to create initial space rooms", + "failed_invite_users": "Failed to invite the following users to your space: %(csvUsers)s", + "invite_teammates_by_username": "Invite by username", + "invite_teammates_description": "Make sure the right people have access. You can invite more later.", + "invite_teammates_heading": "Invite your teammates", + "inviting_users": "Inviting…", + "label": "Create a space", + "name_required": "Please enter a name for the space", + "personal_space": "Just me", + "personal_space_description": "A private space to organise your rooms", + "private_description": "Invite only, best for yourself or teams", + "private_heading": "Your private space", + "private_personal_description": "Make sure the right people have access to %(name)s", + "private_personal_heading": "Who are you working with?", + "private_space": "Me and my teammates", + "private_space_description": "A private space for you and your teammates", + "public_description": "Open space for anyone, best for communities", + "public_heading": "Your public space", + "search_public_button": "Search for public spaces", + "setup_rooms_community_description": "Let's create a room for each of them.", + "setup_rooms_community_heading": "What are some things you want to discuss in %(spaceName)s?", + "setup_rooms_description": "You can add more later too, including already existing ones.", + "setup_rooms_private_description": "We'll create rooms for each of them.", + "setup_rooms_private_heading": "What projects are your team working on?", + "share_description": "It's just you at the moment, it will be even better with others.", + "share_heading": "Share %(name)s", + "skip_action": "Skip for now", + "subspace_adding": "Adding…", + "subspace_beta_notice": "Add a space to a space you manage.", + "subspace_dropdown_title": "Create a space", + "subspace_existing_space_prompt": "Want to add an existing space instead?", + "subspace_join_rule_invite_description": "Only people invited will be able to find and join this space.", + "subspace_join_rule_invite_only": "Private space (invite only)", + "subspace_join_rule_label": "Space visibility", + "subspace_join_rule_public_description": "Anyone will be able to find and join this space, not just members of .", + "subspace_join_rule_restricted_description": "Anyone in will be able to find and join." + }, + "credits": { + "default_cover_photo": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.", + "twemoji": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.", + "twemoji_colr": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0." + }, + "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", + "devtools": { + "active_widgets": "Active Widgets", + "category_other": "Other", + "category_room": "Room", + "caution_colon": "Caution:", + "client_versions": "Client Versions", + "crypto": { + "4s_public_key_in_account_data": "in account data", + "4s_public_key_not_in_account_data": "not found", + "4s_public_key_status": "Secret storage public key:", + "backup_key_cached": "cached locally", + "backup_key_cached_status": "Backup key cached:", + "backup_key_not_stored": "not stored", + "backup_key_stored": "in secret storage", + "backup_key_stored_status": "Backup key stored:", + "backup_key_unexpected_type": "unexpected type", + "backup_key_well_formed": "well formed", + "cross_signing": "Cross-signing", + "cross_signing_cached": "cached locally", + "cross_signing_not_ready": "Cross-signing is not set up.", + "cross_signing_private_keys_in_storage": "in secret storage", + "cross_signing_private_keys_in_storage_status": "Cross-signing private keys:", + "cross_signing_private_keys_not_in_storage": "not found in storage", + "cross_signing_public_keys_on_device": "in memory", + "cross_signing_public_keys_on_device_status": "Cross-signing public keys:", + "cross_signing_ready": "Cross-signing is ready for use.", + "cross_signing_status": "Cross-signing status:", + "cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", + "crypto_not_available": "Cryptographic module is not available", + "key_backup_active_version": "Active backup version:", + "key_backup_active_version_none": "None", + "key_backup_inactive_warning": "Your keys are not being backed up from this session.", + "key_backup_latest_version": "Latest backup version on server:", + "key_storage": "Key Storage", + "master_private_key_cached_status": "Master private key:", + "not_found": "not found", + "not_found_locally": "not found locally", + "secret_storage_not_ready": "not ready", + "secret_storage_ready": "ready", + "secret_storage_status": "Secret storage:", + "self_signing_private_key_cached_status": "Self signing private key:", + "title": "End-to-end encryption", + "user_signing_private_key_cached_status": "User signing private key:" + }, + "developer_mode": "Developer mode", + "developer_tools": "Developer Tools", + "edit_setting": "Edit setting", + "edit_values": "Edit values", + "empty_string": "", + "event_content": "Event Content", + "event_id": "Event ID: %(eventId)s", + "event_sent": "Event sent!", + "event_type": "Event Type", + "explore_account_data": "Explore account data", + "explore_room_account_data": "Explore room account data", + "explore_room_state": "Explore room state", + "failed_to_find_widget": "There was an error finding this widget.", + "failed_to_load": "Failed to load.", + "failed_to_save": "Failed to save settings.", + "failed_to_send": "Failed to send event!", + "id": "ID: ", + "invalid_json": "Doesn't look like valid JSON.", + "level": "Level", + "low_bandwidth_mode": "Low bandwidth mode", + "low_bandwidth_mode_description": "Requires compatible homeserver.", + "main_timeline": "Main timeline", + "no_receipt_found": "No receipt found", + "notification_state": "Notification state is %(notificationState)s", + "notifications_debug": "Notifications debug", + "number_of_users": "Number of users", + "original_event_source": "Original event source", + "room_encrypted": "Room is encrypted ✅", + "room_id": "Room ID: %(roomId)s", + "room_not_encrypted": "Room is not encrypted 🚨", + "room_notifications_dot": "Dot: ", + "room_notifications_highlight": "Highlight: ", + "room_notifications_last_event": "Last event:", + "room_notifications_sender": "Sender: ", + "room_notifications_thread_id": "Thread Id: ", + "room_notifications_total": "Total: ", + "room_notifications_type": "Type: ", + "room_status": "Room status", + "room_unread_status_count": { + "one": "Room unread status: %(status)s, count: %(count)s", + "other": "Room unread status: %(status)s, count: %(count)s" + }, + "save_setting_values": "Save setting values", + "see_history": "See history", + "send_custom_account_data_event": "Send custom account data event", + "send_custom_room_account_data_event": "Send custom room account data event", + "send_custom_state_event": "Send custom state event", + "send_custom_timeline_event": "Send custom timeline event", + "server_info": "Server info", + "server_versions": "Server Versions", + "settable_global": "Settable at global", + "settable_room": "Settable at room", + "setting_colon": "Setting:", + "setting_definition": "Setting definition:", + "setting_id": "Setting ID", + "settings_explorer": "Settings explorer", + "show_hidden_events": "Show hidden events in timeline", + "spaces": { + "one": "", + "other": "<%(count)s spaces>" + }, + "state_key": "State Key", + "thread_root_id": "Thread Root ID: %(threadRootId)s", + "threads_timeline": "Threads timeline", + "title": "Developer tools", + "toggle_event": "toggle event", + "toolbox": "Toolbox", + "use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.", + "user_read_up_to": "User read up to: ", + "user_read_up_to_ignore_synthetic": "User read up to (ignoreSynthetic): ", + "user_read_up_to_private": "User read up to (m.read.private): ", + "user_read_up_to_private_ignore_synthetic": "User read up to (m.read.private;ignoreSynthetic): ", + "value": "Value", + "value_colon": "Value:", + "value_in_this_room": "Value in this room", + "value_this_room_colon": "Value in this room:", + "values_explicit": "Values at explicit levels", + "values_explicit_colon": "Values at explicit levels:", + "values_explicit_room": "Values at explicit levels in this room", + "values_explicit_this_room_colon": "Values at explicit levels in this room:", + "view_servers_in_room": "View servers in room", + "view_source_decrypted_event_source": "Decrypted event source", + "view_source_decrypted_event_source_unavailable": "Decrypted source unavailable", + "widget_screenshots": "Enable widget screenshots on supported widgets" + }, + "dialog_close_label": "Close dialog", + "download_completed": "Download Completed", + "emoji": { + "categories": "Categories", + "category_activities": "Activities", + "category_animals_nature": "Animals & Nature", + "category_flags": "Flags", + "category_food_drink": "Food & Drink", + "category_frequently_used": "Frequently Used", + "category_objects": "Objects", + "category_smileys_people": "Smileys & People", + "category_symbols": "Symbols", + "category_travel_places": "Travel & Places", + "quick_reactions": "Quick Reactions" + }, + "emoji_picker": { + "cancel_search_label": "Cancel search" + }, + "empty_room": "Empty room", + "empty_room_was_name": "Empty room (was %(oldName)s)", + "encryption": { + "access_secret_storage_dialog": { + "enter_phrase_or_key_prompt": "Enter your Security Phrase or to continue.", + "key_validation_text": { + "invalid_security_key": "Invalid Recovery Key", + "recovery_key_is_correct": "Looks good!", + "wrong_file_type": "Wrong file type", + "wrong_security_key": "Wrong Recovery Key" + }, + "reset_title": "Reset everything", + "reset_warning_1": "Only do this if you have no other device to complete verification with.", + "reset_warning_2": "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", + "restoring": "Restoring keys from backup", + "security_key_title": "Recovery Key", + "security_phrase_incorrect_error": "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", + "security_phrase_title": "Security Phrase", + "separator": "%(securityKey)s or %(recoveryFile)s", + "use_security_key_prompt": "Use your Recovery Key to continue." + }, + "bootstrap_title": "Setting up keys", + "cancel_entering_passphrase_description": "Are you sure you want to cancel entering passphrase?", + "cancel_entering_passphrase_title": "Cancel entering passphrase?", + "confirm_encryption_setup_body": "Click the button below to confirm setting up encryption.", + "confirm_encryption_setup_title": "Confirm encryption setup", + "cross_signing_not_ready": "Cross-signing is not set up.", + "cross_signing_ready": "Cross-signing is ready for use.", + "cross_signing_ready_no_backup": "Cross-signing is ready but keys are not backed up.", + "cross_signing_room_normal": "This room is end-to-end encrypted", + "cross_signing_room_verified": "Everyone in this room is verified", + "cross_signing_room_warning": "Someone is using an unknown session", + "cross_signing_unsupported": "Your homeserver does not support cross-signing.", + "cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", + "cross_signing_user_normal": "You have not verified this user.", + "cross_signing_user_verified": "You have verified this user. This user has verified all of their sessions.", + "cross_signing_user_warning": "This user has not verified all of their sessions.", + "destroy_cross_signing_dialog": { + "primary_button_text": "Clear cross-signing keys", + "title": "Destroy cross-signing keys?", + "warning": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from." + }, + "enter_recovery_key": "Enter recovery key", + "event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.", + "event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session", + "event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.", + "event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.", + "event_shield_reason_unverified_identity": "Encrypted by an unverified user.", + "export_unsupported": "Your browser does not support the required cryptography extensions", + "forgot_recovery_key": "Forgot recovery key?", + "import_invalid_keyfile": "Not a valid %(brand)s keyfile", + "import_invalid_passphrase": "Authentication check failed: incorrect password?", + "key_storage_out_of_sync": "Your key storage is out of sync.", + "key_storage_out_of_sync_description": "Confirm your recovery key to maintain access to your key storage and message history.", + "messages_not_secure": { + "cause_1": "Your homeserver", + "cause_2": "The homeserver the user you're verifying is connected to", + "cause_3": "Yours, or the other users' internet connection", + "cause_4": "Yours, or the other users' session", + "heading": "One of the following may be compromised:", + "title": "Your messages are not secure" + }, + "new_recovery_method_detected": { + "description_1": "A new Security Phrase and key for Secure Messages have been detected.", + "description_2": "This session is encrypting history using the new recovery method.", + "title": "New Recovery Method", + "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": "", + "pinned_identity_changed": "%(displayName)s's (%(userId)s) identity appears to have changed. Learn more", + "pinned_identity_changed_no_displayname": "%(userId)s's identity appears to have changed. Learn more", + "recovery_method_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.", + "title": "Recovery Method Removed", + "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." + }, + "reset_all_button": "Forgotten or lost all recovery methods? Reset all", + "set_up_recovery": "Set up recovery", + "set_up_recovery_later": "Not now", + "set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.", + "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", + "set_up_toast_title": "Set up Secure Backup", + "setup_secure_backup": { + "explainer": "Back up your keys before signing out to avoid losing them.", + "title": "Set up" + }, + "udd": { + "interactive_verification_button": "Interactively verify by emoji", + "other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.", + "other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:", + "own_ask_verify_text": "Verify your other session using one of the options below.", + "own_new_session_text": "You signed in to a new session without verifying it:", + "title": "Not Trusted" + }, + "unable_to_setup_keys_error": "Unable to set up keys", + "verification": { + "accepting": "Accepting…", + "after_new_login": { + "device_verified": "Device verified", + "reset_confirmation": "Really reset verification keys?", + "skip_verification": "Skip verification for now", + "unable_to_verify": "Unable to verify this device", + "verify_this_device": "Verify this device" + }, + "cancelled": "You cancelled verification.", + "cancelled_self": "You cancelled verification on your other device.", + "cancelled_user": "%(displayName)s cancelled verification.", + "cancelling": "Cancelling…", + "complete_action": "Got It", + "complete_description": "You've successfully verified this user.", + "complete_title": "Verified!", + "error_starting_description": "We were unable to start a chat with the other user.", + "error_starting_title": "Error starting verification", + "explainer": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", + "in_person": "To be secure, do this in person or use a trusted way to communicate.", + "incoming_sas_device_dialog_text_1": "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", + "incoming_sas_device_dialog_text_2": "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", + "incoming_sas_dialog_title": "Incoming Verification Request", + "incoming_sas_dialog_waiting": "Waiting for partner to confirm…", + "incoming_sas_user_dialog_text_1": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", + "incoming_sas_user_dialog_text_2": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", + "no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", + "no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", + "other_party_cancelled": "The other party cancelled the verification.", + "prompt_encrypted": "Verify all users in a room to ensure it's secure.", + "prompt_self": "Start verification again from the notification.", + "prompt_unencrypted": "In encrypted rooms, verify all users to ensure it's secure.", + "prompt_user": "Start verification again from their profile.", + "qr_or_sas": "%(qrCode)s or %(emojiCompare)s", + "qr_or_sas_header": "Verify this device by completing one of the following:", + "qr_prompt": "Scan this unique code", + "qr_reciprocate_same_shield_device": "Almost there! Is your other device showing the same shield?", + "qr_reciprocate_same_shield_user": "Almost there! Is %(displayName)s showing the same shield?", + "request_toast_accept": "Verify Session", + "request_toast_accept_user": "Verify User", + "request_toast_decline_counter": "Ignore (%(counter)s)", + "request_toast_detail": "%(deviceId)s from %(ip)s", + "reset_proceed_prompt": "Proceed with reset", + "sas_caption_self": "Verify this device by confirming the following number appears on its screen.", + "sas_caption_user": "Verify this user by confirming the following number appears on their screen.", + "sas_description": "Compare a unique set of emoji if you don't have a camera on either device", + "sas_emoji_caption_self": "Confirm the emoji below are displayed on both devices, in the same order:", + "sas_emoji_caption_user": "Verify this user by confirming the following emoji appear on their screen.", + "sas_match": "They match", + "sas_no_match": "They don't match", + "sas_prompt": "Compare unique emoji", + "scan_qr": "Verify by scanning", + "scan_qr_explainer": "Ask %(displayName)s to scan your code:", + "self_verification_hint": "To proceed, please accept the verification request on your other device.", + "start_button": "Start Verification", + "successful_device": "You've successfully verified %(deviceName)s (%(deviceId)s)!", + "successful_own_device": "You've successfully verified your device!", + "successful_user": "You've successfully verified %(displayName)s!", + "timed_out": "Verification timed out.", + "unsupported_method": "Unable to find a supported verification method.", + "unverified_session_toast_accept": "Yes, it was me", + "unverified_session_toast_title": "New login. Was this you?", + "unverified_sessions_toast_description": "Review to ensure your account is safe", + "unverified_sessions_toast_reject": "Later", + "unverified_sessions_toast_title": "You have unverified sessions", + "verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.", + "verification_dialog_title_device": "Verify other device", + "verification_dialog_title_user": "Verification Request", + "verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", + "verification_success_with_backup": "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.", + "verification_success_without_backup": "Your new device is now verified. Other users will see it as trusted.", + "verify_emoji": "Verify by emoji", + "verify_emoji_prompt": "Verify by comparing unique emoji.", + "verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.", + "verify_later": "I'll verify later", + "verify_reset_warning_1": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", + "verify_reset_warning_2": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Key.", + "verify_using_device": "Verify with another device", + "verify_using_key": "Verify with Recovery Key", + "verify_using_key_or_phrase": "Verify with Recovery Key or Phrase", + "waiting_for_user_accept": "Waiting for %(displayName)s to accept…", + "waiting_other_device": "Waiting for you to verify on your other device…", + "waiting_other_device_details": "Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…", + "waiting_other_user": "Waiting for %(displayName)s to verify…" + }, + "verification_requested_toast_title": "Verification requested", + "verified_identity_changed": "%(displayName)s's (%(userId)s) verified identity has changed. Learn more", + "verified_identity_changed_no_displayname": "%(userId)s's verified identity has changed. Learn more", + "verify_toast_description": "Other users may not trust it", + "verify_toast_title": "Verify this session", + "withdraw_verification_action": "Withdraw verification" + }, + "error": { + "admin_contact": "Please contact your service administrator to continue using this service.", + "admin_contact_short": "Contact your server admin.", + "app_launch_unexpected_error": "Unexpected error preparing the app. See console for details.", + "cannot_load_config": "Unable to load config file: please refresh the page to try again.", + "connection": "There was a problem communicating with the homeserver, please try again later.", + "dialog_description_default": "An error has occurred.", + "download_media": "Failed to download source media, no source url was found", + "edit_history_unsupported": "Your homeserver doesn't seem to support this feature.", + "failed_copy": "Failed to copy", + "hs_blocked": "This homeserver has been blocked by its administrator.", + "invalid_configuration_mixed_server": "Invalid configuration: a default_hs_url can't be specified along with default_server_name or default_server_config", + "invalid_configuration_no_server": "Invalid configuration: no default server specified.", + "invalid_json": "Your Element configuration contains invalid JSON. Please correct the problem and reload the page.", + "invalid_json_detail": "The message from the parser is: %(message)s", + "invalid_json_generic": "Invalid JSON", + "mau": "This homeserver has hit its Monthly Active User limit.", + "misconfigured": "Your Element is misconfigured", + "mixed_content": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", + "non_urgent_echo_failure_toast": "Your server isn't responding to some requests.", + "resource_limits": "This homeserver has exceeded one of its resource limits.", + "session_restore": { + "clear_storage_button": "Clear Storage and Sign Out", + "clear_storage_description": "Sign out and remove encryption keys?", + "description_1": "We encountered an error trying to restore your previous session.", + "description_2": "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.", + "description_3": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", + "title": "Unable to restore session" + }, + "something_went_wrong": "Something went wrong!", + "storage_evicted_description_1": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", + "storage_evicted_description_2": "Your browser likely removed this data when running low on disk space.", + "storage_evicted_title": "Missing session data", + "sync": "Unable to connect to Homeserver. Retrying…", + "tls": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + "unknown": "Unknown error", + "unknown_error_code": "unknown error code", + "update_power_level": "Failed to change power level" + }, + "error_app_open_in_another_tab": "Switch to the other tab to connect to %(brand)s. This tab can now be closed.", + "error_app_open_in_another_tab_title": "%(brand)s is connected in another tab", + "error_app_opened_in_another_window": "%(brand)s is open in another window. Click \"%(label)s\" to use %(brand)s here and disconnect the other window.", + "error_database_closed_description": { + "for_desktop": "Your disk may be full. Please clear up some space and reload.", + "for_web": "If you cleared browsing data then this message is expected. %(brand)s may also be open in another tab, or your disk is full. Please clear up some space and reload" + }, + "error_database_closed_title": "%(brand)s stopped working", + "error_dialog": { + "copy_room_link_failed": { + "description": "Unable to copy a link to the room to the clipboard.", + "title": "Unable to copy room link" + }, + "error_loading_user_profile": "Could not load user profile", + "forget_room_failed": "Failed to forget room %(errCode)s", + "search_failed": { + "server_unavailable": "Server may be unavailable, overloaded, or search timed out :(", + "title": "Search failed" + } + }, + "error_user_not_logged_in": "User is not logged in", + "event_preview": { + "m.call.answer": { + "dm": "Call in progress", + "user": "%(senderName)s joined the call", + "you": "You joined the call" + }, + "m.call.hangup": { + "user": "%(senderName)s ended the call", + "you": "You ended the call" + }, + "m.call.invite": { + "dm_receive": "%(senderName)s is calling", + "dm_send": "Waiting for answer", + "user": "%(senderName)s started a call", + "you": "You started a call" + }, + "m.emote": "* %(senderName)s %(emote)s", + "m.reaction": { + "user": "%(sender)s reacted %(reaction)s to %(message)s", + "you": "You reacted %(reaction)s to %(message)s" + }, + "m.sticker": "%(senderName)s: %(stickerName)s", + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "File", + "image": "Image", + "poll": "Poll", + "video": "Video" + }, + "preview": "%(prefix)s: %(preview)s" + }, + "export_chat": { + "cancelled": "Export Cancelled", + "cancelled_detail": "The export was cancelled successfully", + "confirm_stop": "Are you sure you want to stop exporting your data? If you do, you'll need to start over.", + "creating_html": "Creating HTML…", + "creating_output": "Creating output…", + "creator_summary": "%(creatorName)s created this room.", + "current_timeline": "Current Timeline", + "enter_number_between_min_max": "Enter a number between %(min)s and %(max)s", + "error_fetching_file": "Error fetching file", + "export_info": "This is the start of export of . Exported by at %(exportDate)s.", + "export_successful": "Export successful!", + "exported_n_events_in_time": { + "one": "Exported %(count)s event in %(seconds)s seconds", + "other": "Exported %(count)s events in %(seconds)s seconds" + }, + "exporting_your_data": "Exporting your data", + "fetched_n_events": { + "one": "Fetched %(count)s event so far", + "other": "Fetched %(count)s events so far" + }, + "fetched_n_events_in_time": { + "one": "Fetched %(count)s event in %(seconds)ss", + "other": "Fetched %(count)s events in %(seconds)ss" + }, + "fetched_n_events_with_total": { + "one": "Fetched %(count)s event out of %(total)s", + "other": "Fetched %(count)s events out of %(total)s" + }, + "fetching_events": "Fetching events…", + "file_attached": "File Attached", + "format": "Format", + "from_the_beginning": "From the beginning", + "generating_zip": "Generating a ZIP", + "html": "HTML", + "html_title": "Exported Data", + "include_attachments": "Include Attachments", + "json": "JSON", + "media_omitted": "Media omitted", + "media_omitted_file_size": "Media omitted - file size limit exceeded", + "messages": "Messages", + "next_page": "Next group of messages", + "num_messages": "Number of messages", + "num_messages_min_max": "Number of messages can only be a number between %(min)s and %(max)s", + "number_of_messages": "Specify a number of messages", + "previous_page": "Previous group of messages", + "processing": "Processing…", + "processing_event_n": "Processing event %(number)s out of %(total)s", + "select_option": "Select from the options below to export chats from your timeline", + "size_limit": "Size Limit", + "size_limit_min_max": "Size can only be a number between %(min)s MB and %(max)s MB", + "size_limit_postfix": "MB", + "starting_export": "Starting export…", + "successful": "Export Successful", + "successful_detail": "Your export was successful. Find it in your Downloads folder.", + "text": "Plain Text", + "title": "Export Chat", + "topic": "Topic: %(topic)s", + "unload_confirm": "Are you sure you want to exit during this export?" + }, + "failed_load_async_component": "Unable to load! Check your network connectivity and try again.", + "feedback": { + "can_contact_label": "You may contact me if you have any follow up questions", + "comment_label": "Comment", + "existing_issue_link": "Please view existing bugs on Github first. No match? Start a new one.", + "may_contact_label": "You may contact me if you want to follow up or to let me test out upcoming ideas", + "platform_username": "Your platform and username will be noted to help us use your feedback as much as we can.", + "pro_type": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", + "send_feedback_action": "Send feedback", + "sent": "Feedback sent! Thanks, we appreciate it!" + }, + "file_panel": { + "empty_description": "Attach files from chat or just drag and drop them anywhere in a room.", + "empty_heading": "No files visible in this room", + "guest_note": "You must register to use this functionality", + "peek_note": "You must join the room to see its files" + }, + "forward": { + "filter_placeholder": "Search for rooms or people", + "message_preview_heading": "Message preview", + "no_perms_title": "You don't have permission to do this", + "open_room": "Open room", + "send_label": "Send", + "sending": "Sending", + "sent": "Sent" + }, + "identity_server": { + "change": "Change identity server", + "change_prompt": "Disconnect from the identity server and connect to instead?", + "change_server_prompt": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", + "checking": "Checking server", + "description_connected": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", + "description_disconnected": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", + "description_optional": "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.", + "disconnect": "Disconnect identity server", + "disconnect_anyway": "Disconnect anyway", + "disconnect_offline_warning": "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.", + "disconnect_personal_data_warning_1": "You are still sharing your personal data on the identity server .", + "disconnect_personal_data_warning_2": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", + "disconnect_server": "Disconnect from the identity server ?", + "disconnect_warning": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", + "do_not_use": "Do not use an identity server", + "error_connection": "Could not connect to identity server", + "error_invalid": "Not a valid identity server (status code %(code)s)", + "error_invalid_or_terms": "Terms of service not accepted or the identity server is invalid.", + "no_terms": "The identity server you have chosen does not have any terms of service.", + "suggestions": "You should:", + "suggestions_1": "check your browser plugins for anything that might block the identity server (such as Privacy Badger)", + "suggestions_2": "contact the administrators of identity server ", + "suggestions_3": "wait and try again later", + "url": "Identity server (%(server)s)", + "url_field_label": "Enter a new identity server", + "url_not_https": "Identity server URL must be HTTPS" + }, + "in_space": "In %(spaceName)s.", + "in_space1_and_space2": "In spaces %(space1Name)s and %(space2Name)s.", + "in_space_and_n_other_spaces": { + "one": "In %(spaceName)s and one other space.", + "other": "In %(spaceName)s and %(count)s other spaces." + }, + "incompatible_browser": { + "continue": "Continue anyway", + "description": "%(brand)s uses some browser features which are not available in your current browser. %(detail)s", + "detail_can_continue": "If you continue, some features may stop working and there is a risk that you may lose data in the future.", + "detail_no_continue": "Try updating this browser if you're not using the latest version and try again.", + "learn_more": "Learn more", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "For the best experience, use Chrome, Firefox, Edge, or Safari.", + "title": "%(brand)s does not support this browser", + "use_desktop_heading": "Use %(brand)s Desktop instead", + "use_mobile_heading": "Use %(brand)s on mobile instead", + "use_mobile_heading_after_desktop": "Or use our mobile app", + "windows": "Windows (%(bits)s-bit)" + }, + "info_tooltip_title": "Information", + "integration_manager": { + "connecting": "Connecting to integration manager…", + "error_connecting": "The integration manager is offline or it cannot reach your homeserver.", + "error_connecting_heading": "Cannot connect to integration manager", + "explainer": "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "manage_title": "Manage integrations", + "use_im": "Use an integration manager to manage bots, widgets, and sticker packs.", + "use_im_default": "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs." + }, + "integrations": { + "disabled_dialog_description": "Enable '%(manageIntegrations)s' in Settings to do this.", + "disabled_dialog_title": "Integrations are disabled", + "impossible_dialog_description": "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", + "impossible_dialog_title": "Integrations not allowed" + }, + "invite": { + "ask_anyway_description": "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?", + "ask_anyway_label": "Start DM anyway", + "ask_anyway_never_warn_label": "Start DM anyway and never warn me again", + "email_caption": "Invite by email", + "email_limit_one": "Invites by email can only be sent one at a time", + "email_use_default_is": "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", + "email_use_is": "Use an identity server to invite by email. Manage in Settings.", + "error_already_invited_room": "User is already invited to the room", + "error_already_invited_space": "User is already invited to the space", + "error_already_joined_room": "User is already in the room", + "error_already_joined_space": "User is already in the space", + "error_bad_state": "The user must be unbanned before they can be invited.", + "error_dm": "We couldn't create your DM.", + "error_find_room": "Something went wrong trying to invite the users.", + "error_find_user_description": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", + "error_find_user_title": "Failed to find the following users", + "error_invite": "We couldn't invite those users. Please check the users you want to invite and try again.", + "error_permissions_room": "You do not have permission to invite people to this room.", + "error_permissions_space": "You do not have permission to invite people to this space.", + "error_profile_undisclosed": "User may or may not exist", + "error_transfer_multiple_target": "A call can only be transferred to a single user.", + "error_unfederated_room": "This room is unfederated. You cannot invite people from external servers.", + "error_unfederated_space": "This space is unfederated. You cannot invite people from external servers.", + "error_unknown": "Unknown server error", + "error_user_not_found": "User does not exist", + "error_version_unsupported_room": "The user's homeserver does not support the version of the room.", + "error_version_unsupported_space": "The user's homeserver does not support the version of the space.", + "failed_generic": "Operation failed", + "failed_title": "Failed to invite", + "invalid_address": "Unrecognised address", + "name_email_mxid_share_room": "Invite someone using their name, email address, username (like ) or share this room.", + "name_email_mxid_share_space": "Invite someone using their name, email address, username (like ) or share this space.", + "name_mxid_share_room": "Invite someone using their name, username (like ) or share this room.", + "name_mxid_share_space": "Invite someone using their name, username (like ) or share this space.", + "recents_section": "Recent Conversations", + "room_failed_partial": "We sent the others, but the below people couldn't be invited to ", + "room_failed_partial_title": "Some invites couldn't be sent", + "room_failed_title": "Failed to invite users to %(roomName)s", + "send_link_prompt": "Or send invite link", + "start_conversation_name_email_mxid_prompt": "Start a conversation with someone using their name, email address or username (like ).", + "start_conversation_name_mxid_prompt": "Start a conversation with someone using their name or username (like ).", + "suggestions_disclaimer": "Some suggestions may be hidden for privacy.", + "suggestions_disclaimer_prompt": "If you can't see who you're looking for, send them your invite link below.", + "suggestions_section": "Recently Direct Messaged", + "to_room": "Invite to %(roomName)s", + "to_space": "Invite to %(spaceName)s", + "transfer_dial_pad_tab": "Dial pad", + "transfer_user_directory_tab": "User Directory", + "unable_find_profiles_description_default": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", + "unable_find_profiles_invite_label_default": "Invite anyway", + "unable_find_profiles_invite_never_warn_label_default": "Invite anyway and never warn me again", + "unable_find_profiles_title": "The following users may not exist", + "unban_first_title": "User cannot be invited until they are unbanned" + }, + "inviting_user1_and_user2": "Inviting %(user1)s and %(user2)s", + "inviting_user_and_n_others": { + "one": "Inviting %(user)s and one other", + "other": "Inviting %(user)s and %(count)s others" + }, + "items_and_n_others": { + "one": " and one other", + "other": " and %(count)s others" + }, + "keyboard": { + "activate_button": "Activate selected button", + "alt": "Alt", + "autocomplete_cancel": "Cancel autocomplete", + "autocomplete_force": "Force complete", + "autocomplete_navigate_next": "Next autocomplete suggestion", + "autocomplete_navigate_prev": "Previous autocomplete suggestion", + "backspace": "Backspace", + "cancel_reply": "Cancel replying to a message", + "category_autocomplete": "Autocomplete", + "category_calls": "Calls", + "category_navigation": "Navigation", + "category_room_list": "Room List", + "close_dialog_menu": "Close dialog or context menu", + "composer_jump_end": "Jump to end of the composer", + "composer_jump_start": "Jump to start of the composer", + "composer_navigate_next_history": "Navigate to next message in composer history", + "composer_navigate_prev_history": "Navigate to previous message in composer history", + "composer_new_line": "New line", + "composer_redo": "Redo edit", + "composer_toggle_bold": "Toggle Bold", + "composer_toggle_code_block": "Toggle Code Block", + "composer_toggle_italics": "Toggle Italics", + "composer_toggle_link": "Toggle Link", + "composer_toggle_quote": "Toggle Quote", + "composer_undo": "Undo edit", + "control": "Ctrl", + "dismiss_read_marker_and_jump_bottom": "Dismiss read marker and jump to bottom", + "end": "End", + "enter": "Enter", + "escape": "Esc", + "go_home_view": "Go to Home View", + "home": "Home", + "jump_first_message": "Jump to first message", + "jump_last_message": "Jump to last message", + "jump_room_search": "Jump to room search", + "jump_to_read_marker": "Jump to oldest unread message", + "keyboard_shortcuts_tab": "Open this settings tab", + "navigate_next_history": "Next recently visited room or space", + "navigate_next_message_edit": "Navigate to next message to edit", + "navigate_prev_history": "Previous recently visited room or space", + "navigate_prev_message_edit": "Navigate to previous message to edit", + "next_landmark": "Go to next landmark", + "next_room": "Next room or DM", + "next_unread_room": "Next unread room or DM", + "number": "[number]", + "open_user_settings": "Open user settings", + "page_down": "Page Down", + "page_up": "Page Up", + "prev_landmark": "Go to previous landmark", + "prev_room": "Previous room or DM", + "prev_unread_room": "Previous unread room or DM", + "room_list_collapse_section": "Collapse room list section", + "room_list_expand_section": "Expand room list section", + "room_list_navigate_down": "Navigate down in the room list", + "room_list_navigate_up": "Navigate up in the room list", + "room_list_select_room": "Select room from the room list", + "scroll_down_timeline": "Scroll down in the timeline", + "scroll_up_timeline": "Scroll up in the timeline", + "search": "Search (must be enabled)", + "send_sticker": "Send a sticker", + "shift": "Shift", + "space": "Space", + "switch_to_space": "Switch to space by number", + "toggle_hidden_events": "Toggle hidden event visibility", + "toggle_microphone_mute": "Toggle microphone mute", + "toggle_right_panel": "Toggle right panel", + "toggle_space_panel": "Toggle space panel", + "toggle_top_left_menu": "Toggle the top left menu", + "toggle_webcam_mute": "Toggle webcam on/off", + "upload_file": "Upload a file" + }, + "labs": { + "allow_screen_share_only_mode": "Allow screen share only mode", + "ask_to_join": "Enable ask to join", + "automatic_debug_logs": "Automatically send debug logs on any error", + "automatic_debug_logs_decryption": "Automatically send debug logs on decryption errors", + "automatic_debug_logs_key_backup": "Automatically send debug logs when key backup is not functioning", + "beta_description": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.", + "beta_feature": "This is a beta feature", + "beta_feedback_leave_button": "To leave the beta, visit your settings.", + "beta_feedback_title": "%(featureName)s Beta feedback", + "beta_section": "Upcoming features", + "bridge_state": "Show info about bridges in room settings", + "bridge_state_channel": "Channel: ", + "bridge_state_creator": "This bridge was provisioned by .", + "bridge_state_manager": "This bridge is managed by .", + "bridge_state_workspace": "Workspace: ", + "click_for_info": "Click for more info", + "currently_experimental": "Currently experimental.", + "custom_themes": "Support adding custom themes", + "dynamic_room_predecessors": "Dynamic room predecessors", + "dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)", + "element_call_video_rooms": "Element Call video rooms", + "exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages", + "exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.", + "experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", + "experimental_section": "Early previews", + "extended_profiles_msc_support": "Requires your server to support MSC4133", + "feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call", + "feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.", + "group_calls": "New group call experience", + "group_developer": "Developer", + "group_encryption": "Encryption", + "group_experimental": "Experimental", + "group_messaging": "Messaging", + "group_moderation": "Moderation", + "group_profile": "Profile", + "group_rooms": "Rooms", + "group_spaces": "Spaces", + "group_themes": "Themes", + "group_threads": "Threads", + "group_ui": "User interface", + "group_voip": "Voice & Video", + "group_widgets": "Widgets", + "hidebold": "Hide notification dot (only display counters badges)", + "html_topic": "Show HTML representation of room topics", + "join_beta": "Join the beta", + "join_beta_reload": "Joining the beta will reload %(brand)s.", + "jump_to_date": "Jump to date (adds /jumptodate and jump to date headers)", + "jump_to_date_msc_support": "Requires your server to support MSC3030", + "latex_maths": "Render LaTeX maths in messages", + "leave_beta": "Leave the beta", + "leave_beta_reload": "Leaving the beta will reload %(brand)s.", + "location_share_live": "Live Location Sharing", + "location_share_live_description": "Temporary implementation. Locations persist in room history.", + "mjolnir": "New ways to ignore people", + "msc3531_hide_messages_pending_moderation": "Let moderators hide messages pending moderation.", + "new_room_list": "Enable new room list", + "notification_settings": "New Notification Settings", + "notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.", + "notification_settings_beta_title": "Notification Settings", + "notifications": "Enable the notifications panel in the room header", + "release_announcement": "Release announcement", + "render_reaction_images": "Render custom images in reactions", + "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", + "report_to_moderators": "Report to moderators", + "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", + "sliding_sync": "Simplified Sliding Sync mode", + "sliding_sync_description": "Under active development, cannot be disabled.", + "sliding_sync_disabled_notice": "Log out and back in to disable", + "sliding_sync_server_no_support": "Your server lacks support", + "under_active_development": "Under active development.", + "unrealiable_e2e": "Unreliable in encrypted rooms", + "video_rooms": "Video rooms", + "video_rooms_a_new_way_to_chat": "A new way to chat over voice and video in %(brand)s.", + "video_rooms_always_on_voip_channels": "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.", + "video_rooms_beta": "Video rooms are a beta feature", + "video_rooms_faq1_answer": "Use the “+” button in the room section of the left panel.", + "video_rooms_faq1_question": "How can I create a video room?", + "video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.", + "video_rooms_faq2_question": "Can I use text chat alongside the video call?", + "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", + "wysiwyg_composer": "Rich text editor" + }, + "labs_mjolnir": { + "advanced_warning": "⚠ These settings are meant for advanced users.", + "ban_reason": "Ignored/Blocked", + "error_adding_ignore": "Error adding ignored user/server", + "error_adding_list_description": "Please verify the room ID or address and try again.", + "error_adding_list_title": "Error subscribing to list", + "error_removing_ignore": "Error removing ignored user/server", + "error_removing_list_description": "Please try again or view your console for hints.", + "error_removing_list_title": "Error unsubscribing from list", + "explainer_1": "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", + "explainer_2": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", + "lists": "You are currently subscribed to:", + "lists_description_1": "Subscribing to a ban list will cause you to join it!", + "lists_description_2": "If this isn't what you want, please use a different tool to ignore users.", + "lists_heading": "Subscribed lists", + "lists_new_label": "Room ID or address of ban list", + "no_lists": "You are not subscribed to any lists", + "personal_description": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.", + "personal_empty": "You have not ignored anyone.", + "personal_heading": "Personal ban list", + "personal_new_label": "Server or user ID to ignore", + "personal_new_placeholder": "eg: @bot:* or example.org", + "personal_section": "You are currently ignoring:", + "room_name": "My Ban List", + "room_topic": "This is your list of users/servers you have blocked - don't leave the room!", + "rules_empty": "None", + "rules_server": "Server rules", + "rules_title": "Ban list rules - %(roomName)s", + "rules_user": "User rules", + "something_went_wrong": "Something went wrong. Please try again or view your console for hints.", + "title": "Ignored users", + "view_rules": "View rules" + }, + "language_dropdown_label": "Language Dropdown", + "leave_room_dialog": { + "last_person_warning": "You are the only person here. If you leave, no one will be able to join in the future, including you.", + "leave_room_question": "Are you sure you want to leave the room '%(roomName)s'?", + "leave_space_question": "Are you sure you want to leave the space '%(spaceName)s'?", + "room_leave_admin_warning": "You're the only administrator in this room. If you leave, nobody will be able to change room settings or take other important actions.", + "room_leave_mod_warning": "You're the only moderator in this room. If you leave, nobody will be able to change room settings or take other important actions.", + "room_rejoin_warning": "This room is not public. You will not be able to rejoin without an invite.", + "space_rejoin_warning": "This space is not public. You will not be able to rejoin without an invite." + }, + "left_panel": { + "open_dial_pad": "Open dial pad" + }, + "lightbox": { + "rotate_left": "Rotate Left", + "rotate_right": "Rotate Right", + "title": "Image view" + }, + "location_sharing": { + "MapStyleUrlNotConfigured": "This homeserver is not configured to display maps.", + "MapStyleUrlNotReachable": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", + "WebGLNotEnabled": "WebGL is required to display maps, please enable it in your browser settings.", + "click_drop_pin": "Click to drop a pin", + "click_move_pin": "Click to move the pin", + "close_sidebar": "Close sidebar", + "error_fetch_location": "Could not fetch location", + "error_no_perms_description": "You need to have the right permissions in order to share locations in this room.", + "error_no_perms_title": "You don't have permission to share locations", + "error_send_description": "%(brand)s could not send your location. Please try again later.", + "error_send_title": "We couldn't send your location", + "error_sharing_live_location": "An error occurred whilst sharing your live location", + "error_stopping_live_location": "An error occurred while stopping your live location", + "expand_map": "Expand map", + "failed_generic": "Failed to fetch your location. Please try again later.", + "failed_load_map": "Unable to load map", + "failed_permission": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", + "failed_timeout": "Timed out trying to fetch your location. Please try again later.", + "failed_unknown": "Unknown error fetching location. Please try again later.", + "find_my_location": "Find my location", + "live_description": "%(displayName)s's live location", + "live_enable_description": "Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.", + "live_enable_heading": "Live location sharing", + "live_location_active": "You are sharing your live location", + "live_location_enabled": "Live location enabled", + "live_location_ended": "Live location ended", + "live_location_error": "Live location error", + "live_locations_empty": "No live locations", + "live_share_button": "Share for %(duration)s", + "live_toggle_label": "Enable live location sharing", + "live_until": "Live until %(expiryTime)s", + "live_update_time": "Updated %(humanizedUpdateTime)s", + "loading_live_location": "Loading live location…", + "location_not_available": "Location not available", + "map_feedback": "Map feedback", + "mapbox_logo": "Mapbox logo", + "reset_bearing": "Reset bearing to north", + "share_button": "Share location", + "share_type_live": "My live location", + "share_type_own": "My current location", + "share_type_pin": "Drop a Pin", + "share_type_prompt": "What location type do you want to share?", + "toggle_attribution": "Toggle attribution" + }, + "member_list": { + "count": { + "one": "%(count)s Member", + "other": "%(count)s Members" + }, + "filter_placeholder": "Search room members", + "invite_button_no_perms_tooltip": "You do not have permission to invite users", + "invited_label": "Invited", + "no_matches": "No matches", + "power_label": "%(userName)s (power %(powerLevelNumber)s)" + }, + "member_list_back_action_label": "Room members", + "message_edit_dialog_title": "Message edits", + "migrating_crypto": "Hang tight. We are updating %(brand)s to make encryption faster and more reliable.", + "mobile_guide": { + "toast_accept": "Use app", + "toast_description": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", + "toast_title": "Use app for a better experience" + }, + "name_and_id": "%(name)s (%(userId)s)", + "no_more_results": "No more results", + "notif_panel": { + "empty_description": "You have no visible notifications.", + "empty_heading": "You're all caught up" + }, + "notifications": { + "all_messages": "All messages", + "all_messages_description": "Get notified for every message", + "class_global": "Global", + "class_other": "Other", + "default": "Default", + "email_pusher_app_display_name": "Email Notifications", + "enable_prompt_toast_description": "Enable desktop notifications", + "enable_prompt_toast_title": "Notifications", + "enable_prompt_toast_title_from_message_send": "Don't miss a reply", + "error_change_title": "Change notification settings", + "keyword": "Keyword", + "keyword_new": "New keyword", + "level_activity": "Activity", + "level_highlight": "Highlight", + "level_muted": "Muted", + "level_none": "None", + "level_notification": "Notification", + "level_unsent": "Unsent", + "mark_all_read": "Mark all as read", + "mentions_and_keywords": "@mentions & keywords", + "mentions_and_keywords_description": "Get notified only with mentions and keywords as set up in your settings", + "mentions_keywords": "Mentions & keywords", + "message_didnt_send": "Message didn't send. Click for info.", + "mute_description": "You won't get any notifications" + }, + "notifier": { + "m.key.verification.request": "%(name)s is requesting verification" + }, + "onboarding": { + "create_room": "Create a Group Chat", + "explore_rooms": "Explore Public Rooms", + "has_avatar_label": "Great, that'll help people know it's you", + "intro_byline": "Own your conversations.", + "intro_welcome": "Welcome to %(appName)s", + "no_avatar_label": "Add a photo so people know it's you.", + "send_dm": "Send a Direct Message", + "welcome_detail": "Now, let's help you get started", + "welcome_user": "Welcome %(name)s" + }, + "pill": { + "permalink_other_room": "Message in %(room)s", + "permalink_this_room": "Message from %(user)s" + }, + "poll": { + "create_poll_action": "Create Poll", + "create_poll_title": "Create poll", + "disclosed_notes": "Voters see results as soon as they have voted", + "edit_poll_title": "Edit poll", + "end_description": "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.", + "end_message": "The poll has ended. Top answer: %(topAnswer)s", + "end_message_no_votes": "The poll has ended. No votes were cast.", + "end_title": "End Poll", + "error_ending_description": "Sorry, the poll did not end. Please try again.", + "error_ending_title": "Failed to end poll", + "error_voting_description": "Sorry, your vote was not registered. Please try again.", + "error_voting_title": "Vote not registered", + "failed_send_poll_description": "Sorry, the poll you tried to create was not posted.", + "failed_send_poll_title": "Failed to post poll", + "notes": "Results are only revealed when you end the poll", + "options_add_button": "Add option", + "options_heading": "Create options", + "options_label": "Option %(number)s", + "options_placeholder": "Write an option", + "topic_heading": "What is your poll question or topic?", + "topic_label": "Question or topic", + "topic_placeholder": "Write something…", + "total_decryption_errors": "Due to decryption errors, some votes may not be counted", + "total_n_votes": { + "one": "%(count)s vote cast. Vote to see the results", + "other": "%(count)s votes cast. Vote to see the results" + }, + "total_n_votes_voted": { + "one": "Based on %(count)s vote", + "other": "Based on %(count)s votes" + }, + "total_no_votes": "No votes cast", + "total_not_ended": "Results will be visible when the poll is ended", + "type_closed": "Closed poll", + "type_heading": "Poll type", + "type_open": "Open poll", + "unable_edit_description": "Sorry, you can't edit a poll after votes have been cast.", + "unable_edit_title": "Can't edit poll" + }, + "power_level": { + "admin": "Admin", + "custom": "Custom (%(level)s)", + "custom_level": "Custom level", + "default": "Default", + "label": "Power level", + "moderator": "Moderator", + "restricted": "Restricted" + }, + "powered_by_matrix": "Powered by Matrix", + "powered_by_matrix_with_logo": "Decentralised, encrypted chat & collaboration powered by $matrixLogo", + "presence": { + "away": "Away", + "busy": "Busy", + "idle": "Idle", + "idle_for": "Idle for %(duration)s", + "offline": "Offline", + "offline_for": "Offline for %(duration)s", + "online": "Online", + "online_for": "Online for %(duration)s", + "unknown": "Unknown", + "unknown_for": "Unknown for %(duration)s", + "unreachable": "User's server unreachable" + }, + "quick_settings": { + "all_settings": "All settings", + "metaspace_section": "Pin to sidebar", + "sidebar_settings": "More options", + "title": "Quick settings" + }, + "quit_warning": { + "call_in_progress": "You seem to be in a call, are you sure you want to quit?", + "file_upload_in_progress": "You seem to be uploading files, are you sure you want to quit?" + }, + "redact": { + "confirm_button": "Confirm Removal", + "confirm_description": "Are you sure you wish to remove (delete) this event?", + "confirm_description_state": "Note that removing room changes like this could undo the change.", + "error": "You cannot delete this message. (%(code)s)", + "ongoing": "Removing…", + "reason_label": "Reason (optional)" + }, + "reject_invitation_dialog": { + "confirmation": "Are you sure you want to reject the invitation?", + "failed": "Failed to reject invitation", + "title": "Reject invitation" + }, + "report_content": { + "description": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.", + "disagree": "Disagree", + "error_create_room_moderation_bot": "Unable to create room with moderation bot", + "hide_messages_from_user": "Check if you want to hide all current and future messages from this user.", + "ignore_user": "Ignore user", + "illegal_content": "Illegal Content", + "missing_reason": "Please fill why you're reporting.", + "nature": "Please pick a nature and describe what makes this message abusive.", + "nature_disagreement": "What this user is writing is wrong.\nThis will be reported to the room moderators.", + "nature_illegal": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", + "nature_nonstandard_admin": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.", + "nature_nonstandard_admin_encrypted": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", + "nature_other": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", + "nature_spam": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", + "nature_toxic": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", + "other_label": "Other", + "report_content_to_homeserver": "Report Content to Your Homeserver Administrator", + "report_entire_room": "Report the entire room", + "spam_or_propaganda": "Spam or propaganda", + "toxic_behaviour": "Toxic Behaviour" + }, + "restore_key_backup_dialog": { + "count_of_decryption_failures": "Failed to decrypt %(failedCount)s sessions!", + "count_of_successfully_restored_keys": "Successfully restored %(sessionCount)s keys", + "enter_key_description": "Access your secure message history and set up secure messaging by entering your Recovery Key.", + "enter_key_title": "Enter Recovery Key", + "enter_phrase_description": "Access your secure message history and set up secure messaging by entering your Security Phrase.", + "enter_phrase_title": "Enter Security Phrase", + "incorrect_security_phrase_dialog": "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.", + "incorrect_security_phrase_title": "Incorrect Security Phrase", + "key_backup_warning": "Warning: you should only set up key backup from a trusted computer.", + "key_fetch_in_progress": "Fetching keys from server…", + "key_forgotten_text": "If you've forgotten your Recovery Key you can ", + "key_is_invalid": "Not a valid Recovery Key", + "key_is_valid": "This looks like a valid Recovery Key!", + "keys_restored_title": "Keys restored", + "load_error_content": "Unable to load backup status", + "load_keys_progress": "%(completed)s of %(total)s keys restored", + "no_backup_error": "No backup found!", + "phrase_forgotten_text": "If you've forgotten your Security Phrase you can use your Recovery Key or set up new recovery options", + "recovery_key_mismatch_description": "Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.", + "recovery_key_mismatch_title": "Recovery Key mismatch", + "restore_failed_error": "Unable to restore backup" + }, + "right_panel": { + "add_integrations": "Add extensions", + "add_topic": "Add topic", + "extensions_button": "Extensions", + "extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room", + "extensions_empty_title": "Boost productivity with more tools, widgets and bots", + "files_button": "Files", + "pinned_messages": { + "empty_description": "Select a message and choose “%(pinAction)s” to it include here.", + "empty_title": "Pin important messages so that they can be easily discovered", + "header": { + "one": "1 Pinned message", + "other": "%(count)s Pinned messages", + "zero": "Pinned messages" + }, + "limits": { + "other": "You can only pin up to %(count)s widgets" + }, + "menu": "Open menu", + "release_announcement": { + "close": "Ok", + "description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.", + "title": "All new pinned messages" + }, + "reply_thread": "Reply to a thread message", + "unpin_all": { + "button": "Unpin all messages", + "content": "Make sure that you really want to remove all pinned messages. This action can’t be undone.", + "title": "Unpin all messages?" + }, + "view": "View in timeline" + }, + "pinned_messages_button": "Pinned messages", + "poll": { + "active_heading": "Active polls", + "empty_active": "There are no active polls in this room", + "empty_active_load_more": "There are no active polls. Load more polls to view polls for previous months", + "empty_active_load_more_n_days": { + "one": "There are no active polls for the past day. Load more polls to view polls for previous months", + "other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months" + }, + "empty_past": "There are no past polls in this room", + "empty_past_load_more": "There are no past polls. Load more polls to view polls for previous months", + "empty_past_load_more_n_days": { + "one": "There are no past polls for the past day. Load more polls to view polls for previous months", + "other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months" + }, + "final_result": { + "one": "Final result based on %(count)s vote", + "other": "Final result based on %(count)s votes" + }, + "load_more": "Load more polls", + "loading": "Loading polls", + "past_heading": "Past polls", + "view_in_timeline": "View poll in timeline", + "view_poll": "View poll" + }, + "polls_button": "Polls", + "room_summary_card": { + "title": "Room info" + }, + "thread_list": { + "context_menu_label": "Thread options" + }, + "video_room_chat": { + "title": "Chat" + } + }, + "room": { + "3pid_invite_email_not_found_account": "This invite was sent to %(email)s which is not associated with your account", + "3pid_invite_email_not_found_account_room": "This invite to %(roomName)s was sent to %(email)s which is not associated with your account", + "3pid_invite_error_description": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.", + "3pid_invite_error_invite_action": "Try to join anyway", + "3pid_invite_error_invite_subtitle": "You can only join it with a working invite.", + "3pid_invite_error_public_subtitle": "You can still join here.", + "3pid_invite_error_title": "Something went wrong with your invite.", + "3pid_invite_error_title_room": "Something went wrong with your invite to %(roomName)s", + "3pid_invite_no_is_subtitle": "Use an identity server in Settings to receive invites directly in %(brand)s.", + "banned_by": "You were banned by %(memberName)s", + "banned_from_room_by": "You were banned from %(roomName)s by %(memberName)s", + "context_menu": { + "copy_link": "Copy room link", + "favourite": "Favourite", + "forget": "Forget Room", + "low_priority": "Low Priority", + "mark_read": "Mark as read", + "mark_unread": "Mark as unread", + "notifications_default": "Match default setting", + "notifications_mute": "Mute room", + "title": "Room options", + "unfavourite": "Favourited" + }, + "creating_room_text": "We're creating a room with %(names)s", + "dm_invite_action": "Start chatting", + "dm_invite_subtitle": " wants to chat", + "dm_invite_title": "Do you want to chat with %(user)s?", + "drop_file_prompt": "Drop file here to upload", + "edit_topic": "Edit topic", + "error_3pid_invite_email_lookup": "Unable to find user by email", + "error_cancel_knock_title": "Failed to cancel", + "error_join_403": "You need an invite to access this room.", + "error_join_404_1": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.", + "error_join_404_2": "If you know a room address, try joining through that instead.", + "error_join_404_invite": "The person who invited you has already left, or their server is offline.", + "error_join_404_invite_same_hs": "The person who invited you has already left.", + "error_join_connection": "There was an error joining.", + "error_join_incompatible_version_1": "Sorry, your homeserver is too old to participate here.", + "error_join_incompatible_version_2": "Please contact your homeserver administrator.", + "error_join_title": "Failed to join", + "error_jump_to_date": "Server returned %(statusCode)s with error code %(errorCode)s", + "error_jump_to_date_connection": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", + "error_jump_to_date_details": "Error details", + "error_jump_to_date_not_found": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", + "error_jump_to_date_send_logs_prompt": "Please submit debug logs to help us track down the problem.", + "error_jump_to_date_title": "Unable to find event at that date", + "face_pile_summary": { + "one": "%(count)s person you know has already joined", + "other": "%(count)s people you know have already joined" + }, + "face_pile_tooltip_label": { + "one": "View 1 member", + "other": "View all %(count)s members" + }, + "face_pile_tooltip_shortcut": "Including %(commaSeparatedMembers)s", + "face_pile_tooltip_shortcut_joined": "Including you, %(commaSeparatedMembers)s", + "failed_reject_invite": "Failed to reject invite", + "forget_room": "Forget this room", + "forget_space": "Forget this space", + "header": { + "n_people_asking_to_join": { + "one": "Asking to join", + "other": "%(count)s people asking to join" + }, + "room_is_public": "This room is public" + }, + "header_avatar_open_settings_label": "Open room settings", + "header_face_pile_tooltip": "People", + "header_untrusted_label": "Untrusted", + "inaccessible": "This room or space is not accessible at this time.", + "inaccessible_name": "%(roomName)s is not accessible at this time.", + "inaccessible_subtitle_1": "Try again later, or ask a room or space admin to check if you have access.", + "inaccessible_subtitle_2": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.", + "intro": { + "dm_caption": "Only the two of you are in this conversation, unless either of you invites anyone to join.", + "enable_encryption_prompt": "Enable encryption in settings.", + "encrypted_3pid_dm_pending_join": "Once everyone has joined, you’ll be able to chat", + "no_avatar_label": "Add a photo, so people can easily spot your room.", + "no_topic": "Add a topic to help people know what it is about.", + "private_unencrypted_warning": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", + "room_invite": "Invite to just this room", + "send_message_start_dm": "Send your first message to invite to chat", + "start_of_dm_history": "This is the beginning of your direct message history with .", + "start_of_room": "This is the start of .", + "topic": "Topic: %(topic)s ", + "topic_edit": "Topic: %(topic)s (edit)", + "unencrypted_warning": "End-to-end encryption isn't enabled", + "user_created": "%(displayName)s created this room.", + "you_created": "You created this room." + }, + "invite_email_mismatch_suggestion": "Share this email in Settings to receive invites directly in %(brand)s.", + "invite_reject_ignore": "Reject & Ignore user", + "invite_sent_to_email": "This invite was sent to %(email)s", + "invite_sent_to_email_room": "This invite to %(roomName)s was sent to %(email)s", + "invite_subtitle": "Invited by ", + "invite_this_room": "Invite to this room", + "invite_title": "Do you want to join %(roomName)s?", + "inviter_unknown": "Unknown", + "invites_you_text": " invites you", + "join_button_account": "Sign Up", + "join_failed_needs_invite": "To view %(roomName)s, you need an invite", + "join_the_discussion": "Join the discussion", + "join_title": "Join the room to participate", + "join_title_account": "Join the conversation with an account", + "joining": "Joining…", + "joining_room": "Joining room…", + "joining_space": "Joining space…", + "jump_read_marker": "Jump to first unread message.", + "jump_to_bottom_button": "Scroll to most recent messages", + "jump_to_date": "Jump to date", + "jump_to_date_beginning": "The beginning of the room", + "jump_to_date_prompt": "Pick a date to jump to", + "kick_reason": "Reason: %(reason)s", + "kicked_by": "You were removed by %(memberName)s", + "kicked_from_room_by": "You were removed from %(roomName)s by %(memberName)s", + "knock_cancel_action": "Cancel request", + "knock_denied_subtitle": "As you have been denied access, you cannot rejoin unless you are invited by the admin or moderator of the group.", + "knock_denied_title": "You have been denied access", + "knock_message_field_placeholder": "Message (optional)", + "knock_prompt": "Ask to join?", + "knock_prompt_name": "Ask to join %(roomName)s?", + "knock_send_action": "Request access", + "knock_sent": "Request to join sent", + "knock_sent_subtitle": "Your request to join is pending.", + "knock_subtitle": "You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.", + "leave_error_title": "Error leaving room", + "leave_server_notices_description": "This room is used for important messages from the Homeserver, so you cannot leave it.", + "leave_server_notices_title": "Can't leave Server Notices room", + "leave_unexpected_error": "Unexpected server error trying to leave the room", + "link_email_to_receive_3pid_invite": "Link this email with your account in Settings to receive invites directly in %(brand)s.", + "loading_preview": "Loading preview", + "no_peek_join_prompt": "%(roomName)s can't be previewed. Do you want to join it?", + "no_peek_no_name_join_prompt": "There's no preview, would you like to join?", + "not_found_subtitle": "Are you sure you're at the right place?", + "not_found_title": "This room or space does not exist.", + "not_found_title_name": "%(roomName)s does not exist.", + "peek_join_prompt": "You're previewing %(roomName)s. Want to join it?", + "pinned_message_badge": "Pinned message", + "pinned_message_banner": { + "button_close_list": "Close list", + "button_view_all": "View all", + "description": "This room has pinned messages. Click to view them.", + "go_to_message": "View the pinned message in the timeline.", + "title": "%(index)s of %(length)s Pinned messages" + }, + "read_topic": "Click to read topic", + "rejecting": "Rejecting invite…", + "rejoin_button": "Re-join", + "search": { + "all_rooms_button": "Search all rooms", + "placeholder": "Search messages…", + "summary": { + "one": "1 result found for “”", + "other": "%(count)s results found for “”" + }, + "this_room_button": "Search this room" + }, + "status_bar": { + "delete_all": "Delete all", + "exceeded_resource_limit": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", + "homeserver_blocked": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", + "monthly_user_limit_reached": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", + "requires_consent_agreement": "You can't send any messages until you review and agree to our terms and conditions.", + "retry_all": "Retry all", + "select_messages_to_retry": "You can select all or individual messages to retry or delete", + "server_connectivity_lost_description": "Sent messages will be stored until your connection has returned.", + "server_connectivity_lost_title": "Connectivity to the server has been lost.", + "some_messages_not_sent": "Some of your messages have not been sent" + }, + "unknown_status_code_for_timeline_jump": "unknown status code", + "unread_notifications_predecessor": { + "one": "You have %(count)s unread notification in a prior version of this room.", + "other": "You have %(count)s unread notifications in a prior version of this room." + }, + "upgrade_error_description": "Double check that your server supports the room version chosen and try again.", + "upgrade_error_title": "Error upgrading room", + "upgrade_warning_bar": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", + "upgrade_warning_bar_admins": "Only room administrators will see this warning", + "upgrade_warning_bar_unstable": "This room is running room version , which this homeserver has marked as unstable.", + "upgrade_warning_bar_upgraded": "This room has already been upgraded.", + "upload": { + "uploading_multiple_file": { + "one": "Uploading %(filename)s and %(count)s other", + "other": "Uploading %(filename)s and %(count)s others" + }, + "uploading_single_file": "Uploading %(filename)s" + }, + "waiting_for_join_subtitle": "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", + "waiting_for_join_title": "Waiting for users to join %(brand)s" + }, + "room_list": { + "add_room_label": "Add room", + "add_space_label": "Add space", + "breadcrumbs_empty": "No recently visited rooms", + "breadcrumbs_label": "Recently visited rooms", + "failed_add_tag": "Failed to add tag %(tagName)s to room", + "failed_remove_tag": "Failed to remove tag %(tagName)s from room", + "failed_set_dm_tag": "Failed to set direct message tag", + "home_menu_label": "Home options", + "join_public_room_label": "Join public room", + "joining_rooms_status": { + "one": "Currently joining %(count)s room", + "other": "Currently joining %(count)s rooms" + }, + "notification_options": "Notification options", + "redacting_messages_status": { + "one": "Currently removing messages in %(count)s room", + "other": "Currently removing messages in %(count)s rooms" + }, + "show_less": "Show less", + "show_n_more": { + "one": "Show %(count)s more", + "other": "Show %(count)s more" + }, + "show_previews": "Show previews of messages", + "sort_by": "Sort by", + "sort_by_activity": "Activity", + "sort_by_alphabet": "A-Z", + "sort_unread_first": "Show rooms with unread messages first", + "space_menu_label": "%(spaceName)s menu", + "sublist_options": "List options", + "suggested_rooms_heading": "Suggested Rooms" + }, + "room_settings": { + "access": { + "description_space": "Decide who can view and join %(spaceName)s.", + "title": "Access" + }, + "advanced": { + "error_upgrade_description": "The room upgrade could not be completed", + "error_upgrade_title": "Failed to upgrade room", + "information_section_room": "Room information", + "information_section_space": "Space information", + "room_id": "Internal room ID", + "room_predecessor": "View older messages in %(roomName)s.", + "room_upgrade_button": "Upgrade this room to the recommended room version", + "room_upgrade_warning": "Warning: upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.", + "room_version": "Room version:", + "room_version_section": "Room version", + "space_predecessor": "View older version of %(spaceName)s.", + "space_upgrade_button": "Upgrade this space to the recommended room version", + "unfederated": "This room is not accessible by remote Matrix servers", + "upgrade_button": "Upgrade this room to version %(version)s", + "upgrade_dialog_description": "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:", + "upgrade_dialog_description_1": "Create a new room with the same name, description and avatar", + "upgrade_dialog_description_2": "Update any local room aliases to point to the new room", + "upgrade_dialog_description_3": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", + "upgrade_dialog_description_4": "Put a link back to the old room at the start of the new room so people can see old messages", + "upgrade_dialog_title": "Upgrade Room Version", + "upgrade_dwarning_ialog_title_public": "Upgrade public room", + "upgrade_warning_dialog_description": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", + "upgrade_warning_dialog_explainer": "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.", + "upgrade_warning_dialog_footer": "You'll upgrade this room from to .", + "upgrade_warning_dialog_invite_label": "Automatically invite members from this room to the new one", + "upgrade_warning_dialog_report_bug_prompt": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", + "upgrade_warning_dialog_report_bug_prompt_link": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", + "upgrade_warning_dialog_title": "Upgrade room", + "upgrade_warning_dialog_title_private": "Upgrade private room" + }, + "alias_not_specified": "not specified", + "bridges": { + "description": "This room is bridging messages to the following platforms. Learn more.", + "empty": "This room isn't bridging messages to any platforms. Learn more.", + "title": "Bridges" + }, + "delete_avatar_label": "Delete avatar", + "general": { + "alias_field_has_domain_invalid": "Missing domain separator e.g. (:domain.org)", + "alias_field_has_localpart_invalid": "Missing room name or separator e.g. (my-room:domain.org)", + "alias_field_matches_invalid": "This address does not point at this room", + "alias_field_placeholder_default": "e.g. my-room", + "alias_field_required_invalid": "Please provide an address", + "alias_field_safe_localpart_invalid": "Some characters not allowed", + "alias_field_taken_invalid": "This address had invalid server or is already in use", + "alias_field_taken_invalid_domain": "This address is already in use", + "alias_field_taken_valid": "This address is available to use", + "alias_heading": "Room address", + "aliases_items_label": "Other published addresses:", + "aliases_no_items_label": "No other published addresses yet, add one below", + "aliases_section": "Room Addresses", + "avatar_field_label": "Room avatar", + "canonical_alias_field_label": "Main address", + "default_url_previews_off": "URL previews are disabled by default for participants in this room.", + "default_url_previews_on": "URL previews are enabled by default for participants in this room.", + "description_space": "Edit settings relating to your space.", + "error_creating_alias_description": "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.", + "error_creating_alias_title": "Error creating address", + "error_deleting_alias_description": "There was an error removing that address. It may no longer exist or a temporary error occurred.", + "error_deleting_alias_description_forbidden": "You don't have permission to delete the address.", + "error_deleting_alias_title": "Error removing address", + "error_publishing": "Unable to publish room", + "error_publishing_detail": "There was an error publishing this room", + "error_save_space_settings": "Failed to save space settings.", + "error_updating_alias_description": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", + "error_updating_canonical_alias_description": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", + "error_updating_canonical_alias_title": "Error updating main address", + "leave_space": "Leave Space", + "local_alias_field_label": "Local address", + "local_aliases_explainer_room": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", + "local_aliases_explainer_space": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", + "local_aliases_section": "Local Addresses", + "name_field_label": "Room Name", + "new_alias_placeholder": "New published address (e.g. #alias:server)", + "no_aliases_room": "This room has no local addresses", + "no_aliases_space": "This space has no local addresses", + "other_section": "Other", + "publish_toggle": "Publish this room to the public in %(domain)s's room directory?", + "published_aliases_description": "To publish an address, it needs to be set as a local address first.", + "published_aliases_explainer_room": "Published addresses can be used by anyone on any server to join your room.", + "published_aliases_explainer_space": "Published addresses can be used by anyone on any server to join your space.", + "published_aliases_section": "Published Addresses", + "save": "Save Changes", + "topic_field_label": "Room Topic", + "url_preview_encryption_warning": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", + "url_preview_explainer": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", + "url_previews_section": "URL Previews", + "user_url_previews_default_off": "You have disabled URL previews by default.", + "user_url_previews_default_on": "You have enabled URL previews by default." + }, + "notifications": { + "browse_button": "Browse", + "custom_sound_prompt": "Set a new custom sound", + "notification_sound": "Notification sound", + "settings_link": "Get notifications as set up in your settings", + "sounds_section": "Sounds", + "upload_sound_label": "Upload custom sound", + "uploaded_sound": "Uploaded sound" + }, + "people": { + "knock_empty": "No requests", + "knock_section": "Asking to join", + "see_less": "See less", + "see_more": "See more" + }, + "permissions": { + "add_privileged_user_description": "Give one or multiple users in this room more privileges", + "add_privileged_user_filter_placeholder": "Search users in this room…", + "add_privileged_user_heading": "Add privileged users", + "ban": "Ban users", + "ban_reason": "Reason", + "banned_by": "Banned by %(displayName)s", + "banned_users_section": "Banned users", + "error_changing_pl_description": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.", + "error_changing_pl_reqs_description": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", + "error_changing_pl_reqs_title": "Error changing power level requirement", + "error_changing_pl_title": "Error changing power level", + "error_unbanning": "Failed to unban", + "events_default": "Send messages", + "invite": "Invite users", + "kick": "Remove users", + "m.call": "Start %(brand)s calls", + "m.call.member": "Join %(brand)s calls", + "m.reaction": "Send reactions", + "m.room.avatar": "Change room avatar", + "m.room.avatar_space": "Change space avatar", + "m.room.canonical_alias": "Change main address for the room", + "m.room.canonical_alias_space": "Change main address for the space", + "m.room.encryption": "Enable room encryption", + "m.room.history_visibility": "Change history visibility", + "m.room.name": "Change room name", + "m.room.name_space": "Change space name", + "m.room.pinned_events": "Manage pinned events", + "m.room.power_levels": "Change permissions", + "m.room.redaction": "Remove messages sent by me", + "m.room.server_acl": "Change server ACLs", + "m.room.tombstone": "Upgrade the room", + "m.room.topic": "Change topic", + "m.room.topic_space": "Change description", + "m.space.child": "Manage rooms in this space", + "m.widget": "Modify widgets", + "muted_users_section": "Muted Users", + "no_privileged_users": "No users have specific privileges in this room", + "notifications.room": "Notify everyone", + "permissions_section": "Permissions", + "permissions_section_description_room": "Select the roles required to change various parts of the room", + "permissions_section_description_space": "Select the roles required to change various parts of the space", + "privileged_users_section": "Privileged Users", + "redact": "Remove messages sent by others", + "send_event_type": "Send %(eventType)s events", + "state_default": "Change settings", + "title": "Roles & Permissions", + "users_default": "Default role" + }, + "security": { + "enable_encryption_confirm_description": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", + "enable_encryption_confirm_title": "Enable encryption?", + "enable_encryption_public_room_confirm_description_1": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", + "enable_encryption_public_room_confirm_description_2": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", + "enable_encryption_public_room_confirm_title": "Are you sure you want to add encryption to this public room?", + "encrypted_room_public_confirm_description_1": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.", + "encrypted_room_public_confirm_description_2": "To avoid these issues, create a new public room for the conversation you plan to have.", + "encrypted_room_public_confirm_title": "Are you sure you want to make this encrypted room public?", + "encryption_forced": "Your server requires encryption to be disabled.", + "encryption_permanent": "Once enabled, encryption cannot be disabled.", + "error_join_rule_change_title": "Failed to update the join rules", + "error_join_rule_change_unknown": "Unknown failure", + "guest_access_warning": "People with supported clients will be able to join the room without having a registered account.", + "history_visibility_invited": "Members only (since they were invited)", + "history_visibility_joined": "Members only (since they joined)", + "history_visibility_legend": "Who can read history?", + "history_visibility_shared": "Members only (since the point in time of selecting this option)", + "history_visibility_warning": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.", + "history_visibility_world_readable": "Anyone", + "join_rule_description": "Decide who can join %(roomName)s.", + "join_rule_invite": "Private (invite only)", + "join_rule_invite_description": "Only invited people can join.", + "join_rule_knock": "Ask to join", + "join_rule_knock_description": "People cannot join unless access is granted.", + "join_rule_public_description": "Anyone can find and join.", + "join_rule_restricted": "Space members", + "join_rule_restricted_description": "Anyone in a space can find and join. Edit which spaces can access here.", + "join_rule_restricted_description_active_space": "Anyone in can find and join. You can select other spaces too.", + "join_rule_restricted_description_prompt": "Anyone in a space can find and join. You can select multiple spaces.", + "join_rule_restricted_description_spaces": "Spaces with access", + "join_rule_restricted_dialog_description": "Decide which spaces can access this room. If a space is selected, its members can find and join .", + "join_rule_restricted_dialog_empty_warning": "You're removing all spaces. Access will default to invite only", + "join_rule_restricted_dialog_filter_placeholder": "Search spaces", + "join_rule_restricted_dialog_heading_known": "Other spaces you know", + "join_rule_restricted_dialog_heading_other": "Other spaces or rooms you might not know", + "join_rule_restricted_dialog_heading_room": "Spaces you know that contain this room", + "join_rule_restricted_dialog_heading_space": "Spaces you know that contain this space", + "join_rule_restricted_dialog_heading_unknown": "These are likely ones other room admins are a part of.", + "join_rule_restricted_dialog_title": "Select spaces", + "join_rule_restricted_n_more": { + "one": "& %(count)s more", + "other": "& %(count)s more" + }, + "join_rule_restricted_summary": { + "one": "Currently, a space has access", + "other": "Currently, %(count)s spaces have access" + }, + "join_rule_restricted_upgrade_description": "This upgrade will allow members of selected spaces access to this room without an invite.", + "join_rule_restricted_upgrade_warning": "This room is in some spaces you're not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.", + "join_rule_upgrade_awaiting_room": "Loading new room", + "join_rule_upgrade_required": "Upgrade required", + "join_rule_upgrade_sending_invites": { + "one": "Sending invite...", + "other": "Sending invites... (%(progress)s out of %(count)s)" + }, + "join_rule_upgrade_updating_spaces": { + "one": "Updating space...", + "other": "Updating spaces... (%(progress)s out of %(count)s)" + }, + "join_rule_upgrade_upgrading_room": "Upgrading room", + "public_without_alias_warning": "To link to this room, please add an address.", + "publish_room": "Make this room visible in the public room directory.", + "publish_space": "Make this space visible in the public room directory.", + "strict_encryption": "Never send encrypted messages to unverified sessions in this room from this session", + "title": "Security & Privacy" + }, + "title": "Room Settings - %(roomName)s", + "upload_avatar_label": "Upload avatar", + "visibility": { + "alias_section": "Address", + "error_failed_save": "Failed to update the visibility of this space", + "error_update_guest_access": "Failed to update the guest access of this space", + "error_update_history_visibility": "Failed to update the history visibility of this space", + "guest_access_explainer": "Guests can join a space without having an account.", + "guest_access_explainer_public_space": "This may be useful for public spaces.", + "guest_access_label": "Enable guest access", + "history_visibility_anyone_space": "Preview Space", + "history_visibility_anyone_space_description": "Allow people to preview your space before they join.", + "history_visibility_anyone_space_recommendation": "Recommended for public spaces.", + "title": "Visibility" + }, + "voip": { + "call_type_section": "Call type", + "enable_element_call_caption": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", + "enable_element_call_label": "Enable %(brand)s as an additional calling option in this room", + "enable_element_call_no_permissions_tooltip": "You do not have sufficient permissions to change this." + } + }, + "room_summary_card_back_action_label": "Room info", + "scalar": { + "error_create": "Unable to create widget.", + "error_membership": "You are not in this room.", + "error_missing_room_id": "Missing roomId.", + "error_missing_room_id_request": "Missing room_id in request", + "error_missing_user_id_request": "Missing user_id in request", + "error_permission": "You do not have permission to do that in this room.", + "error_power_level_invalid": "Power level must be positive integer.", + "error_room_not_visible": "Room %(roomId)s not visible", + "error_room_unknown": "This room is not recognised.", + "error_send_request": "Failed to send request.", + "failed_read_event": "Failed to read events", + "failed_send_event": "Failed to send event" + }, + "server_offline": { + "description": "Your server isn't responding to some of your requests. Below are some of the most likely reasons.", + "description_1": "The server (%(serverName)s) took too long to respond.", + "description_2": "Your firewall or anti-virus is blocking the request.", + "description_3": "A browser extension is preventing the request.", + "description_4": "The server is offline.", + "description_5": "The server has denied your request.", + "description_6": "Your area is experiencing difficulties connecting to the internet.", + "description_7": "A connection error occurred while trying to contact the server.", + "description_8": "The server is not configured to indicate what the problem is (CORS).", + "empty_timeline": "You're all caught up.", + "recent_changes_heading": "Recent changes that have not yet been received", + "title": "Server isn't responding" + }, + "seshat": { + "error_initialising": "Message search initialisation failed, check your settings for more information", + "reset_button": "Reset event store", + "reset_description": "You most likely do not want to reset your event index store", + "reset_explainer": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated", + "reset_title": "Reset event store?", + "warning_kind_files": "This version of %(brand)s does not support viewing some encrypted files", + "warning_kind_files_app": "Use the Desktop app to see all encrypted files", + "warning_kind_search": "This version of %(brand)s does not support searching encrypted messages", + "warning_kind_search_app": "Use the Desktop app to search encrypted messages" + }, + "setting": { + "help_about": { + "access_token_detail": "Your access token gives full access to your account. Do not share it with anyone.", + "brand_version": "%(brand)s version:", + "clear_cache_reload": "Clear cache and reload", + "crypto_version": "Crypto version:", + "dialog_title": "Settings: Help & About", + "help_link": "For help with using %(brand)s, click here.", + "homeserver": "Homeserver is %(homeserverUrl)s", + "identity_server": "Identity server is %(identityServerUrl)s", + "title": "Help & About", + "versions": "Versions" + } + }, + "settings": { + "account": { + "dialog_title": "Settings: Account", + "title": "Account" + }, + "all_rooms_home": "Show all rooms in Home", + "all_rooms_home_description": "All rooms you're in will appear in Home.", + "always_show_message_timestamps": "Always show message timestamps", + "appearance": { + "bundled_emoji_font": "Use bundled emoji font", + "compact_layout": "Show compact text and messages", + "compact_layout_description": "Modern layout must be selected to use this feature.", + "custom_font": "Use a system font", + "custom_font_description": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", + "custom_font_name": "System font name", + "custom_font_size": "Use custom size", + "custom_theme_add": "Add custom theme", + "custom_theme_downloading": "Downloading custom theme…", + "custom_theme_error_downloading": "Error downloading theme", + "custom_theme_help": "Enter the URL of a custom theme you want to apply.", + "custom_theme_invalid": "Invalid theme schema.", + "dialog_title": "Settings: Appearance", + "font_size": "Font size", + "font_size_default": "%(fontSize)s (default)", + "high_contrast": "High contrast", + "image_size_default": "Default", + "image_size_large": "Large", + "layout_bubbles": "Message bubbles", + "layout_irc": "IRC (experimental)", + "match_system_theme": "Match system theme", + "timeline_image_size": "Image size in the timeline" + }, + "automatic_language_detection_syntax_highlight": "Enable automatic language detection for syntax highlighting", + "autoplay_gifs": "Autoplay GIFs", + "autoplay_videos": "Autoplay videos", + "big_emoji": "Enable big emoji in chat", + "code_block_expand_default": "Expand code blocks by default", + "code_block_line_numbers": "Show line numbers in code blocks", + "disable_historical_profile": "Show current profile picture and name for users in message history", + "discovery": { + "title": "How to find you" + }, + "emoji_autocomplete": "Enable Emoji suggestions while typing", + "enable_markdown": "Enable Markdown", + "enable_markdown_description": "Start messages with /plain to send without markdown.", "encryption": { - "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Enter your Security Phrase or to continue.", - "key_validation_text": { - "invalid_security_key": "Invalid Recovery Key", - "recovery_key_is_correct": "Looks good!", - "wrong_file_type": "Wrong file type", - "wrong_security_key": "Wrong Recovery Key" - }, - "reset_title": "Reset everything", - "reset_warning_1": "Only do this if you have no other device to complete verification with.", - "reset_warning_2": "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", - "restoring": "Restoring keys from backup", - "security_key_title": "Recovery Key", - "security_phrase_incorrect_error": "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", - "security_phrase_title": "Security Phrase", - "separator": "%(securityKey)s or %(recoveryFile)s", - "use_security_key_prompt": "Use your Recovery Key to continue." - }, - "bootstrap_title": "Setting up keys", - "cancel_entering_passphrase_description": "Are you sure you want to cancel entering passphrase?", - "cancel_entering_passphrase_title": "Cancel entering passphrase?", - "confirm_encryption_setup_body": "Click the button below to confirm setting up encryption.", - "confirm_encryption_setup_title": "Confirm encryption setup", - "cross_signing_not_ready": "Cross-signing is not set up.", - "cross_signing_ready": "Cross-signing is ready for use.", - "cross_signing_ready_no_backup": "Cross-signing is ready but keys are not backed up.", - "cross_signing_room_normal": "This room is end-to-end encrypted", - "cross_signing_room_verified": "Everyone in this room is verified", - "cross_signing_room_warning": "Someone is using an unknown session", - "cross_signing_unsupported": "Your homeserver does not support cross-signing.", - "cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", - "cross_signing_user_normal": "You have not verified this user.", - "cross_signing_user_verified": "You have verified this user. This user has verified all of their sessions.", - "cross_signing_user_warning": "This user has not verified all of their sessions.", - "destroy_cross_signing_dialog": { - "primary_button_text": "Clear cross-signing keys", - "title": "Destroy cross-signing keys?", - "warning": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from." - }, + "advanced": { + "breadcrumb_first_description": "Your account details, contacts, preferences, and chat list will be kept", + "breadcrumb_page": "Reset encryption", + "breadcrumb_second_description": "You will lose any message history that’s stored only on the server", + "breadcrumb_third_description": "You will need to verify all your existing devices and contacts again", + "breadcrumb_title": "Are you sure you want to reset your identity?", + "breadcrumb_title_forgot": "Forgot your recovery key? You’ll need to reset your identity.", + "breadcrumb_warning": "Only do this if you believe your account has been compromised.", + "details_title": "Encryption details", + "export_keys": "Export keys", + "import_keys": "Import keys", + "other_people_device_description": "By default in encrypted rooms, do not send encrypted messages to anyone until you’ve verified them", + "other_people_device_label": "Never send encrypted messages to unverified devices", + "other_people_device_title": "Other people’s devices", + "reset_identity": "Reset cryptographic identity", + "session_id": "Session ID:", + "session_key": "Session key:", + "title": "Advanced" + }, + "device_not_verified_button": "Verify this device", + "device_not_verified_description": "You need to verify this device in order to view your encryption settings.", + "device_not_verified_title": "Device not verified", + "dialog_title": "Settings: Encryption", + "recovery": { + "change_recovery_confirm_button": "Confirm new recovery key", + "change_recovery_confirm_description": "Enter your new recovery key below to finish. Your old one will no longer work.", + "change_recovery_confirm_title": "Enter your new recovery key", + "change_recovery_key": "Change recovery key", + "change_recovery_key_description": "Write down this new recovery key somewhere safe. Then click Continue to confirm the change.", + "change_recovery_key_title": "Change recovery key?", + "description": "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.", + "enter_key_error": "The recovery key you entered is not correct.", "enter_recovery_key": "Enter recovery key", - "event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.", - "event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session", - "event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.", - "event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.", - "event_shield_reason_unverified_identity": "Encrypted by an unverified user.", - "export_unsupported": "Your browser does not support the required cryptography extensions", "forgot_recovery_key": "Forgot recovery key?", - "import_invalid_keyfile": "Not a valid %(brand)s keyfile", - "import_invalid_passphrase": "Authentication check failed: incorrect password?", - "key_storage_out_of_sync": "Your key storage is out of sync.", - "key_storage_out_of_sync_description": "Confirm your recovery key to maintain access to your key storage and message history.", - "messages_not_secure": { - "cause_1": "Your homeserver", - "cause_2": "The homeserver the user you're verifying is connected to", - "cause_3": "Yours, or the other users' internet connection", - "cause_4": "Yours, or the other users' session", - "heading": "One of the following may be compromised:", - "title": "Your messages are not secure" - }, - "new_recovery_method_detected": { - "description_1": "A new Security Phrase and key for Secure Messages have been detected.", - "description_2": "This session is encrypting history using the new recovery method.", - "title": "New Recovery Method", - "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": "", - "pinned_identity_changed": "%(displayName)s's (%(userId)s) identity appears to have changed. Learn more", - "pinned_identity_changed_no_displayname": "%(userId)s's identity appears to have changed. Learn more", - "recovery_method_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.", - "title": "Recovery Method Removed", - "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." - }, - "reset_all_button": "Forgotten or lost all recovery methods? Reset all", + "key_storage_warning": "Your key storage is out of sync. Click one of the buttons below to fix the problem.", + "save_key_description": "Do not share this with anyone!", + "save_key_title": "Recovery key", "set_up_recovery": "Set up recovery", - "set_up_recovery_later": "Not now", - "set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.", - "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", - "set_up_toast_title": "Set up Secure Backup", - "setup_secure_backup": { - "explainer": "Back up your keys before signing out to avoid losing them.", - "title": "Set up" - }, - "udd": { - "interactive_verification_button": "Interactively verify by emoji", - "other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.", - "other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:", - "own_ask_verify_text": "Verify your other session using one of the options below.", - "own_new_session_text": "You signed in to a new session without verifying it:", - "title": "Not Trusted" - }, - "unable_to_setup_keys_error": "Unable to set up keys", - "verification": { - "accepting": "Accepting…", - "after_new_login": { - "device_verified": "Device verified", - "reset_confirmation": "Really reset verification keys?", - "skip_verification": "Skip verification for now", - "unable_to_verify": "Unable to verify this device", - "verify_this_device": "Verify this device" - }, - "cancelled": "You cancelled verification.", - "cancelled_self": "You cancelled verification on your other device.", - "cancelled_user": "%(displayName)s cancelled verification.", - "cancelling": "Cancelling…", - "complete_action": "Got It", - "complete_description": "You've successfully verified this user.", - "complete_title": "Verified!", - "error_starting_description": "We were unable to start a chat with the other user.", - "error_starting_title": "Error starting verification", - "explainer": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", - "in_person": "To be secure, do this in person or use a trusted way to communicate.", - "incoming_sas_device_dialog_text_1": "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", - "incoming_sas_device_dialog_text_2": "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", - "incoming_sas_dialog_title": "Incoming Verification Request", - "incoming_sas_dialog_waiting": "Waiting for partner to confirm…", - "incoming_sas_user_dialog_text_1": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", - "incoming_sas_user_dialog_text_2": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", - "no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", - "no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", - "other_party_cancelled": "The other party cancelled the verification.", - "prompt_encrypted": "Verify all users in a room to ensure it's secure.", - "prompt_self": "Start verification again from the notification.", - "prompt_unencrypted": "In encrypted rooms, verify all users to ensure it's secure.", - "prompt_user": "Start verification again from their profile.", - "qr_or_sas": "%(qrCode)s or %(emojiCompare)s", - "qr_or_sas_header": "Verify this device by completing one of the following:", - "qr_prompt": "Scan this unique code", - "qr_reciprocate_same_shield_device": "Almost there! Is your other device showing the same shield?", - "qr_reciprocate_same_shield_user": "Almost there! Is %(displayName)s showing the same shield?", - "request_toast_accept": "Verify Session", - "request_toast_accept_user": "Verify User", - "request_toast_decline_counter": "Ignore (%(counter)s)", - "request_toast_detail": "%(deviceId)s from %(ip)s", - "reset_proceed_prompt": "Proceed with reset", - "sas_caption_self": "Verify this device by confirming the following number appears on its screen.", - "sas_caption_user": "Verify this user by confirming the following number appears on their screen.", - "sas_description": "Compare a unique set of emoji if you don't have a camera on either device", - "sas_emoji_caption_self": "Confirm the emoji below are displayed on both devices, in the same order:", - "sas_emoji_caption_user": "Verify this user by confirming the following emoji appear on their screen.", - "sas_match": "They match", - "sas_no_match": "They don't match", - "sas_prompt": "Compare unique emoji", - "scan_qr": "Verify by scanning", - "scan_qr_explainer": "Ask %(displayName)s to scan your code:", - "self_verification_hint": "To proceed, please accept the verification request on your other device.", - "start_button": "Start Verification", - "successful_device": "You've successfully verified %(deviceName)s (%(deviceId)s)!", - "successful_own_device": "You've successfully verified your device!", - "successful_user": "You've successfully verified %(displayName)s!", - "timed_out": "Verification timed out.", - "unsupported_method": "Unable to find a supported verification method.", - "unverified_session_toast_accept": "Yes, it was me", - "unverified_session_toast_title": "New login. Was this you?", - "unverified_sessions_toast_description": "Review to ensure your account is safe", - "unverified_sessions_toast_reject": "Later", - "unverified_sessions_toast_title": "You have unverified sessions", - "verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.", - "verification_dialog_title_device": "Verify other device", - "verification_dialog_title_user": "Verification Request", - "verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", - "verification_success_with_backup": "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.", - "verification_success_without_backup": "Your new device is now verified. Other users will see it as trusted.", - "verify_emoji": "Verify by emoji", - "verify_emoji_prompt": "Verify by comparing unique emoji.", - "verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.", - "verify_later": "I'll verify later", - "verify_reset_warning_1": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", - "verify_reset_warning_2": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Key.", - "verify_using_device": "Verify with another device", - "verify_using_key": "Verify with Recovery Key", - "verify_using_key_or_phrase": "Verify with Recovery Key or Phrase", - "waiting_for_user_accept": "Waiting for %(displayName)s to accept…", - "waiting_other_device": "Waiting for you to verify on your other device…", - "waiting_other_device_details": "Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…", - "waiting_other_user": "Waiting for %(displayName)s to verify…" - }, - "verification_requested_toast_title": "Verification requested", - "verified_identity_changed": "%(displayName)s's (%(userId)s) verified identity has changed. Learn more", - "verified_identity_changed_no_displayname": "%(userId)s's verified identity has changed. Learn more", - "verify_toast_description": "Other users may not trust it", - "verify_toast_title": "Verify this session", - "withdraw_verification_action": "Withdraw verification" - }, - "error": { - "admin_contact": "Please contact your service administrator to continue using this service.", - "admin_contact_short": "Contact your server admin.", - "app_launch_unexpected_error": "Unexpected error preparing the app. See console for details.", - "cannot_load_config": "Unable to load config file: please refresh the page to try again.", - "connection": "There was a problem communicating with the homeserver, please try again later.", - "dialog_description_default": "An error has occurred.", - "download_media": "Failed to download source media, no source url was found", - "edit_history_unsupported": "Your homeserver doesn't seem to support this feature.", - "failed_copy": "Failed to copy", - "hs_blocked": "This homeserver has been blocked by its administrator.", - "invalid_configuration_mixed_server": "Invalid configuration: a default_hs_url can't be specified along with default_server_name or default_server_config", - "invalid_configuration_no_server": "Invalid configuration: no default server specified.", - "invalid_json": "Your Element configuration contains invalid JSON. Please correct the problem and reload the page.", - "invalid_json_detail": "The message from the parser is: %(message)s", - "invalid_json_generic": "Invalid JSON", - "mau": "This homeserver has hit its Monthly Active User limit.", - "misconfigured": "Your Element is misconfigured", - "mixed_content": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", - "non_urgent_echo_failure_toast": "Your server isn't responding to some requests.", - "resource_limits": "This homeserver has exceeded one of its resource limits.", - "session_restore": { - "clear_storage_button": "Clear Storage and Sign Out", - "clear_storage_description": "Sign out and remove encryption keys?", - "description_1": "We encountered an error trying to restore your previous session.", - "description_2": "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.", - "description_3": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", - "title": "Unable to restore session" - }, - "something_went_wrong": "Something went wrong!", - "storage_evicted_description_1": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", - "storage_evicted_description_2": "Your browser likely removed this data when running low on disk space.", - "storage_evicted_title": "Missing session data", - "sync": "Unable to connect to Homeserver. Retrying…", - "tls": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - "unknown": "Unknown error", - "unknown_error_code": "unknown error code", - "update_power_level": "Failed to change power level" - }, - "error_app_open_in_another_tab": "Switch to the other tab to connect to %(brand)s. This tab can now be closed.", - "error_app_open_in_another_tab_title": "%(brand)s is connected in another tab", - "error_app_opened_in_another_window": "%(brand)s is open in another window. Click \"%(label)s\" to use %(brand)s here and disconnect the other window.", - "error_database_closed_description": { - "for_desktop": "Your disk may be full. Please clear up some space and reload.", - "for_web": "If you cleared browsing data then this message is expected. %(brand)s may also be open in another tab, or your disk is full. Please clear up some space and reload" - }, - "error_database_closed_title": "%(brand)s stopped working", - "error_dialog": { - "copy_room_link_failed": { - "description": "Unable to copy a link to the room to the clipboard.", - "title": "Unable to copy room link" - }, - "error_loading_user_profile": "Could not load user profile", - "forget_room_failed": "Failed to forget room %(errCode)s", - "search_failed": { - "server_unavailable": "Server may be unavailable, overloaded, or search timed out :(", - "title": "Search failed" - } - }, - "error_user_not_logged_in": "User is not logged in", - "event_preview": { - "m.call.answer": { - "dm": "Call in progress", - "user": "%(senderName)s joined the call", - "you": "You joined the call" - }, - "m.call.hangup": { - "user": "%(senderName)s ended the call", - "you": "You ended the call" - }, - "m.call.invite": { - "dm_receive": "%(senderName)s is calling", - "dm_send": "Waiting for answer", - "user": "%(senderName)s started a call", - "you": "You started a call" - }, - "m.emote": "* %(senderName)s %(emote)s", - "m.reaction": { - "user": "%(sender)s reacted %(reaction)s to %(message)s", - "you": "You reacted %(reaction)s to %(message)s" - }, - "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s", - "prefix": { - "audio": "Audio", - "file": "File", - "image": "Image", - "poll": "Poll", - "video": "Video" - }, - "preview": "%(prefix)s: %(preview)s" - }, - "export_chat": { - "cancelled": "Export Cancelled", - "cancelled_detail": "The export was cancelled successfully", - "confirm_stop": "Are you sure you want to stop exporting your data? If you do, you'll need to start over.", - "creating_html": "Creating HTML…", - "creating_output": "Creating output…", - "creator_summary": "%(creatorName)s created this room.", - "current_timeline": "Current Timeline", - "enter_number_between_min_max": "Enter a number between %(min)s and %(max)s", - "error_fetching_file": "Error fetching file", - "export_info": "This is the start of export of . Exported by at %(exportDate)s.", - "export_successful": "Export successful!", - "exported_n_events_in_time": { - "one": "Exported %(count)s event in %(seconds)s seconds", - "other": "Exported %(count)s events in %(seconds)s seconds" - }, - "exporting_your_data": "Exporting your data", - "fetched_n_events": { - "one": "Fetched %(count)s event so far", - "other": "Fetched %(count)s events so far" - }, - "fetched_n_events_in_time": { - "one": "Fetched %(count)s event in %(seconds)ss", - "other": "Fetched %(count)s events in %(seconds)ss" - }, - "fetched_n_events_with_total": { - "one": "Fetched %(count)s event out of %(total)s", - "other": "Fetched %(count)s events out of %(total)s" - }, - "fetching_events": "Fetching events…", - "file_attached": "File Attached", - "format": "Format", - "from_the_beginning": "From the beginning", - "generating_zip": "Generating a ZIP", - "html": "HTML", - "html_title": "Exported Data", - "include_attachments": "Include Attachments", - "json": "JSON", - "media_omitted": "Media omitted", - "media_omitted_file_size": "Media omitted - file size limit exceeded", - "messages": "Messages", - "next_page": "Next group of messages", - "num_messages": "Number of messages", - "num_messages_min_max": "Number of messages can only be a number between %(min)s and %(max)s", - "number_of_messages": "Specify a number of messages", - "previous_page": "Previous group of messages", - "processing": "Processing…", - "processing_event_n": "Processing event %(number)s out of %(total)s", - "select_option": "Select from the options below to export chats from your timeline", - "size_limit": "Size Limit", - "size_limit_min_max": "Size can only be a number between %(min)s MB and %(max)s MB", - "size_limit_postfix": "MB", - "starting_export": "Starting export…", - "successful": "Export Successful", - "successful_detail": "Your export was successful. Find it in your Downloads folder.", - "text": "Plain Text", - "title": "Export Chat", - "topic": "Topic: %(topic)s", - "unload_confirm": "Are you sure you want to exit during this export?" - }, - "failed_load_async_component": "Unable to load! Check your network connectivity and try again.", - "feedback": { - "can_contact_label": "You may contact me if you have any follow up questions", - "comment_label": "Comment", - "existing_issue_link": "Please view existing bugs on Github first. No match? Start a new one.", - "may_contact_label": "You may contact me if you want to follow up or to let me test out upcoming ideas", - "platform_username": "Your platform and username will be noted to help us use your feedback as much as we can.", - "pro_type": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", - "send_feedback_action": "Send feedback", - "sent": "Feedback sent! Thanks, we appreciate it!" - }, - "file_panel": { - "empty_description": "Attach files from chat or just drag and drop them anywhere in a room.", - "empty_heading": "No files visible in this room", - "guest_note": "You must register to use this functionality", - "peek_note": "You must join the room to see its files" - }, - "forward": { - "filter_placeholder": "Search for rooms or people", - "message_preview_heading": "Message preview", - "no_perms_title": "You don't have permission to do this", - "open_room": "Open room", - "send_label": "Send", - "sending": "Sending", - "sent": "Sent" - }, - "identity_server": { - "change": "Change identity server", - "change_prompt": "Disconnect from the identity server and connect to instead?", - "change_server_prompt": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", - "checking": "Checking server", - "description_connected": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", - "description_disconnected": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", - "description_optional": "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.", - "disconnect": "Disconnect identity server", - "disconnect_anyway": "Disconnect anyway", - "disconnect_offline_warning": "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.", - "disconnect_personal_data_warning_1": "You are still sharing your personal data on the identity server .", - "disconnect_personal_data_warning_2": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", - "disconnect_server": "Disconnect from the identity server ?", - "disconnect_warning": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", - "do_not_use": "Do not use an identity server", - "error_connection": "Could not connect to identity server", - "error_invalid": "Not a valid identity server (status code %(code)s)", - "error_invalid_or_terms": "Terms of service not accepted or the identity server is invalid.", - "no_terms": "The identity server you have chosen does not have any terms of service.", - "suggestions": "You should:", - "suggestions_1": "check your browser plugins for anything that might block the identity server (such as Privacy Badger)", - "suggestions_2": "contact the administrators of identity server ", - "suggestions_3": "wait and try again later", - "url": "Identity server (%(server)s)", - "url_field_label": "Enter a new identity server", - "url_not_https": "Identity server URL must be HTTPS" - }, - "in_space": "In %(spaceName)s.", - "in_space1_and_space2": "In spaces %(space1Name)s and %(space2Name)s.", - "in_space_and_n_other_spaces": { - "one": "In %(spaceName)s and one other space.", - "other": "In %(spaceName)s and %(count)s other spaces." - }, - "incompatible_browser": { - "continue": "Continue anyway", - "description": "%(brand)s uses some browser features which are not available in your current browser. %(detail)s", - "detail_can_continue": "If you continue, some features may stop working and there is a risk that you may lose data in the future.", - "detail_no_continue": "Try updating this browser if you're not using the latest version and try again.", - "learn_more": "Learn more", - "linux": "Linux", - "macos": "Mac", - "supported_browsers": "For the best experience, use Chrome, Firefox, Edge, or Safari.", - "title": "%(brand)s does not support this browser", - "use_desktop_heading": "Use %(brand)s Desktop instead", - "use_mobile_heading": "Use %(brand)s on mobile instead", - "use_mobile_heading_after_desktop": "Or use our mobile app", - "windows": "Windows (%(bits)s-bit)" - }, - "info_tooltip_title": "Information", - "integration_manager": { - "connecting": "Connecting to integration manager…", - "error_connecting": "The integration manager is offline or it cannot reach your homeserver.", - "error_connecting_heading": "Cannot connect to integration manager", - "explainer": "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", - "manage_title": "Manage integrations", - "use_im": "Use an integration manager to manage bots, widgets, and sticker packs.", - "use_im_default": "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs." - }, - "integrations": { - "disabled_dialog_description": "Enable '%(manageIntegrations)s' in Settings to do this.", - "disabled_dialog_title": "Integrations are disabled", - "impossible_dialog_description": "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", - "impossible_dialog_title": "Integrations not allowed" - }, - "invite": { - "ask_anyway_description": "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?", - "ask_anyway_label": "Start DM anyway", - "ask_anyway_never_warn_label": "Start DM anyway and never warn me again", - "email_caption": "Invite by email", - "email_limit_one": "Invites by email can only be sent one at a time", - "email_use_default_is": "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", - "email_use_is": "Use an identity server to invite by email. Manage in Settings.", - "error_already_invited_room": "User is already invited to the room", - "error_already_invited_space": "User is already invited to the space", - "error_already_joined_room": "User is already in the room", - "error_already_joined_space": "User is already in the space", - "error_bad_state": "The user must be unbanned before they can be invited.", - "error_dm": "We couldn't create your DM.", - "error_find_room": "Something went wrong trying to invite the users.", - "error_find_user_description": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", - "error_find_user_title": "Failed to find the following users", - "error_invite": "We couldn't invite those users. Please check the users you want to invite and try again.", - "error_permissions_room": "You do not have permission to invite people to this room.", - "error_permissions_space": "You do not have permission to invite people to this space.", - "error_profile_undisclosed": "User may or may not exist", - "error_transfer_multiple_target": "A call can only be transferred to a single user.", - "error_unfederated_room": "This room is unfederated. You cannot invite people from external servers.", - "error_unfederated_space": "This space is unfederated. You cannot invite people from external servers.", - "error_unknown": "Unknown server error", - "error_user_not_found": "User does not exist", - "error_version_unsupported_room": "The user's homeserver does not support the version of the room.", - "error_version_unsupported_space": "The user's homeserver does not support the version of the space.", - "failed_generic": "Operation failed", - "failed_title": "Failed to invite", - "invalid_address": "Unrecognised address", - "name_email_mxid_share_room": "Invite someone using their name, email address, username (like ) or share this room.", - "name_email_mxid_share_space": "Invite someone using their name, email address, username (like ) or share this space.", - "name_mxid_share_room": "Invite someone using their name, username (like ) or share this room.", - "name_mxid_share_space": "Invite someone using their name, username (like ) or share this space.", - "recents_section": "Recent Conversations", - "room_failed_partial": "We sent the others, but the below people couldn't be invited to ", - "room_failed_partial_title": "Some invites couldn't be sent", - "room_failed_title": "Failed to invite users to %(roomName)s", - "send_link_prompt": "Or send invite link", - "start_conversation_name_email_mxid_prompt": "Start a conversation with someone using their name, email address or username (like ).", - "start_conversation_name_mxid_prompt": "Start a conversation with someone using their name or username (like ).", - "suggestions_disclaimer": "Some suggestions may be hidden for privacy.", - "suggestions_disclaimer_prompt": "If you can't see who you're looking for, send them your invite link below.", - "suggestions_section": "Recently Direct Messaged", - "to_room": "Invite to %(roomName)s", - "to_space": "Invite to %(spaceName)s", - "transfer_dial_pad_tab": "Dial pad", - "transfer_user_directory_tab": "User Directory", - "unable_find_profiles_description_default": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", - "unable_find_profiles_invite_label_default": "Invite anyway", - "unable_find_profiles_invite_never_warn_label_default": "Invite anyway and never warn me again", - "unable_find_profiles_title": "The following users may not exist", - "unban_first_title": "User cannot be invited until they are unbanned" - }, - "inviting_user1_and_user2": "Inviting %(user1)s and %(user2)s", - "inviting_user_and_n_others": { - "one": "Inviting %(user)s and one other", - "other": "Inviting %(user)s and %(count)s others" - }, - "items_and_n_others": { - "one": " and one other", - "other": " and %(count)s others" + "set_up_recovery_confirm_button": "Finish set up", + "set_up_recovery_confirm_description": "Enter the recovery key shown on the previous screen to finish setting up recovery.", + "set_up_recovery_confirm_title": "Enter your recovery key to confirm", + "set_up_recovery_description": "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘%(changeRecoveryKeyButton)s’.", + "set_up_recovery_save_key_description": "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.", + "set_up_recovery_save_key_title": "Save your recovery key somewhere safe", + "set_up_recovery_secondary_description": "After clicking continue, we’ll generate a recovery key for you.", + "title": "Recovery" + }, + "title": "Encryption" + }, + "general": { + "account_management_section": "Account management", + "account_section": "Account", + "add_email_dialog_title": "Add Email Address", + "add_email_failed_verification": "Failed to verify email address: make sure you clicked the link in the email", + "add_email_instructions": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", + "add_msisdn_confirm_body": "Click the button below to confirm adding this phone number.", + "add_msisdn_confirm_button": "Confirm adding phone number", + "add_msisdn_confirm_sso_button": "Confirm adding this phone number by using Single Sign On to prove your identity.", + "add_msisdn_dialog_title": "Add Phone Number", + "add_msisdn_instructions": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", + "add_msisdn_misconfigured": "The add / bind with MSISDN flow is misconfigured", + "allow_spellcheck": "Allow spell check", + "application_language": "Application language", + "application_language_reload_hint": "The app will reload after selecting another language", + "avatar_remove_progress": "Removing image...", + "avatar_save_progress": "Uploading image...", + "avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.", + "avatar_upload_error_text_generic": "The file format may not be supported.", + "avatar_upload_error_title": "Avatar image could not be uploaded", + "confirm_adding_email_body": "Click the button below to confirm adding this email address.", + "confirm_adding_email_title": "Confirm adding email", + "deactivate_confirm_body": "Are you sure you want to deactivate your account? This is irreversible.", + "deactivate_confirm_body_sso": "Confirm your account deactivation by using Single Sign On to prove your identity.", + "deactivate_confirm_content": "Confirm that you would like to deactivate your account. If you proceed:", + "deactivate_confirm_content_1": "You will not be able to reactivate your account", + "deactivate_confirm_content_2": "You will no longer be able to log in", + "deactivate_confirm_content_3": "No one will be able to reuse your username (MXID), including you: this username will remain unavailable", + "deactivate_confirm_content_4": "You will leave all rooms and DMs that you are in", + "deactivate_confirm_content_5": "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number", + "deactivate_confirm_content_6": "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?", + "deactivate_confirm_continue": "Confirm account deactivation", + "deactivate_confirm_erase_label": "Hide my messages from new joiners", + "deactivate_section": "Deactivate Account", + "deactivate_warning": "Deactivating your account is a permanent action — be careful!", + "discovery_email_empty": "Discovery options will appear once you have added an email.", + "discovery_email_verification_instructions": "Verify the link in your inbox", + "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number.", + "discovery_needs_terms": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", + "discovery_needs_terms_title": "Let people find you", + "display_name": "Display Name", + "display_name_error": "Unable to set display name", + "email_address_in_use": "This email address is already in use", + "email_address_label": "Email Address", + "email_not_verified": "Your email address hasn't been verified yet", + "email_verification_instructions": "Click the link in the email you received to verify and then click continue again.", + "emails_heading": "Email addresses", + "error_add_email": "Unable to add email address", + "error_deactivate_communication": "There was a problem communicating with the server. Please try again.", + "error_deactivate_invalid_auth": "Server did not return valid authentication information.", + "error_deactivate_no_auth": "Server did not require any authentication", + "error_email_verification": "Unable to verify email address.", + "error_invalid_email": "Invalid Email Address", + "error_invalid_email_detail": "This doesn't appear to be a valid email address", + "error_msisdn_verification": "Unable to verify phone number.", + "error_password_change_403": "Failed to change password. Is your password correct?", + "error_password_change_http": "%(errorMessage)s (HTTP status %(httpStatus)s)", + "error_password_change_title": "Error changing password", + "error_password_change_unknown": "Unknown password change error (%(stringifiedError)s)", + "error_remove_3pid": "Unable to remove contact information", + "error_revoke_email_discovery": "Unable to revoke sharing for email address", + "error_revoke_msisdn_discovery": "Unable to revoke sharing for phone number", + "error_share_email_discovery": "Unable to share email address", + "error_share_msisdn_discovery": "Unable to share phone number", + "identity_server_no_token": "No identity access token found", + "identity_server_not_set": "Identity server not set", + "language_section": "Language", + "msisdn_in_use": "This phone number is already in use", + "msisdn_label": "Phone Number", + "msisdn_verification_field_label": "Verification code", + "msisdn_verification_instructions": "Please enter verification code sent via text.", + "msisdns_heading": "Phone numbers", + "oidc_manage_button": "Manage account", + "password_change_section": "Set a new account password…", + "password_change_success": "Your password was successfully changed.", + "personal_info": "Personal info", + "profile_subtitle": "This is how you appear to others on the app.", + "profile_subtitle_oidc": "Your account is managed separately by an identity provider and so some of your personal information can’t be changed here.", + "remove_email_prompt": "Remove %(email)s?", + "remove_msisdn_prompt": "Remove %(phone)s?", + "spell_check_locale_placeholder": "Choose a locale", + "unable_to_load_emails": "Unable to load email addresses", + "unable_to_load_msisdns": "Unable to load phone numbers", + "username": "Username" + }, + "image_thumbnails": "Show previews/thumbnails for images", + "inline_url_previews_default": "Enable inline URL previews by default", + "inline_url_previews_room": "Enable URL previews by default for participants in this room", + "inline_url_previews_room_account": "Enable URL previews for this room (only affects you)", + "insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message", + "jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message", + "key_backup": { + "backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).", + "backup_starting": "Starting backup…", + "backup_success": "Success!", + "cannot_create_backup": "Unable to create key backup", + "create_title": "Create key backup", + "setup_secure_backup": { + "backup_setup_success_description": "Your keys are now being backed up from this device.", + "backup_setup_success_title": "Secure Backup successful", + "cancel_warning": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", + "confirm_security_phrase": "Confirm your Security Phrase", + "description": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", + "download_or_copy": "%(downloadButton)s or %(copyButton)s", + "enter_phrase_description": "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", + "enter_phrase_title": "Enter a Security Phrase", + "enter_phrase_to_confirm": "Enter your Security Phrase a second time to confirm it.", + "generate_security_key_description": "We'll generate a Recovery Key for you to store somewhere safe, like a password manager or a safe.", + "generate_security_key_title": "Generate a Recovery Key", + "pass_phrase_match_failed": "That doesn't match.", + "pass_phrase_match_success": "That matches!", + "phrase_strong_enough": "Great! This Security Phrase looks strong enough.", + "secret_storage_query_failure": "Unable to query secret storage status", + "security_key_safety_reminder": "Store your Recovery Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", + "set_phrase_again": "Go back to set it again.", + "settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.", + "title_confirm_phrase": "Confirm Security Phrase", + "title_save_key": "Save your Recovery Key", + "title_set_phrase": "Set a Security Phrase", + "unable_to_setup": "Unable to set up secret storage", + "use_different_passphrase": "Use a different passphrase?", + "use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Recovery Key to use for backup." + } + }, + "key_export_import": { + "confirm_passphrase": "Confirm passphrase", + "enter_passphrase": "Enter passphrase", + "export_description_1": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", + "export_description_2": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", + "export_title": "Export room keys", + "file_to_import": "File to import", + "import_description_1": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", + "import_description_2": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", + "import_title": "Import room keys", + "phrase_cannot_be_empty": "Passphrase must not be empty", + "phrase_must_match": "Passphrases must match", + "phrase_strong_enough": "Great! This passphrase looks strong enough" }, "keyboard": { - "activate_button": "Activate selected button", - "alt": "Alt", - "autocomplete_cancel": "Cancel autocomplete", - "autocomplete_force": "Force complete", - "autocomplete_navigate_next": "Next autocomplete suggestion", - "autocomplete_navigate_prev": "Previous autocomplete suggestion", - "backspace": "Backspace", - "cancel_reply": "Cancel replying to a message", - "category_autocomplete": "Autocomplete", - "category_calls": "Calls", - "category_navigation": "Navigation", - "category_room_list": "Room List", - "close_dialog_menu": "Close dialog or context menu", - "composer_jump_end": "Jump to end of the composer", - "composer_jump_start": "Jump to start of the composer", - "composer_navigate_next_history": "Navigate to next message in composer history", - "composer_navigate_prev_history": "Navigate to previous message in composer history", - "composer_new_line": "New line", - "composer_redo": "Redo edit", - "composer_toggle_bold": "Toggle Bold", - "composer_toggle_code_block": "Toggle Code Block", - "composer_toggle_italics": "Toggle Italics", - "composer_toggle_link": "Toggle Link", - "composer_toggle_quote": "Toggle Quote", - "composer_undo": "Undo edit", - "control": "Ctrl", - "dismiss_read_marker_and_jump_bottom": "Dismiss read marker and jump to bottom", - "end": "End", - "enter": "Enter", - "escape": "Esc", - "go_home_view": "Go to Home View", - "home": "Home", - "jump_first_message": "Jump to first message", - "jump_last_message": "Jump to last message", - "jump_room_search": "Jump to room search", - "jump_to_read_marker": "Jump to oldest unread message", - "keyboard_shortcuts_tab": "Open this settings tab", - "navigate_next_history": "Next recently visited room or space", - "navigate_next_message_edit": "Navigate to next message to edit", - "navigate_prev_history": "Previous recently visited room or space", - "navigate_prev_message_edit": "Navigate to previous message to edit", - "next_landmark": "Go to next landmark", - "next_room": "Next room or DM", - "next_unread_room": "Next unread room or DM", - "number": "[number]", - "open_user_settings": "Open user settings", - "page_down": "Page Down", - "page_up": "Page Up", - "prev_landmark": "Go to previous landmark", - "prev_room": "Previous room or DM", - "prev_unread_room": "Previous unread room or DM", - "room_list_collapse_section": "Collapse room list section", - "room_list_expand_section": "Expand room list section", - "room_list_navigate_down": "Navigate down in the room list", - "room_list_navigate_up": "Navigate up in the room list", - "room_list_select_room": "Select room from the room list", - "scroll_down_timeline": "Scroll down in the timeline", - "scroll_up_timeline": "Scroll up in the timeline", - "search": "Search (must be enabled)", - "send_sticker": "Send a sticker", - "shift": "Shift", - "space": "Space", - "switch_to_space": "Switch to space by number", - "toggle_hidden_events": "Toggle hidden event visibility", - "toggle_microphone_mute": "Toggle microphone mute", - "toggle_right_panel": "Toggle right panel", - "toggle_space_panel": "Toggle space panel", - "toggle_top_left_menu": "Toggle the top left menu", - "toggle_webcam_mute": "Toggle webcam on/off", - "upload_file": "Upload a file" + "dialog_title": "Settings: Keyboard", + "title": "Keyboard" }, "labs": { - "allow_screen_share_only_mode": "Allow screen share only mode", - "ask_to_join": "Enable ask to join", - "automatic_debug_logs": "Automatically send debug logs on any error", - "automatic_debug_logs_decryption": "Automatically send debug logs on decryption errors", - "automatic_debug_logs_key_backup": "Automatically send debug logs when key backup is not functioning", - "beta_description": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.", - "beta_feature": "This is a beta feature", - "beta_feedback_leave_button": "To leave the beta, visit your settings.", - "beta_feedback_title": "%(featureName)s Beta feedback", - "beta_section": "Upcoming features", - "bridge_state": "Show info about bridges in room settings", - "bridge_state_channel": "Channel: ", - "bridge_state_creator": "This bridge was provisioned by .", - "bridge_state_manager": "This bridge is managed by .", - "bridge_state_workspace": "Workspace: ", - "click_for_info": "Click for more info", - "currently_experimental": "Currently experimental.", - "custom_themes": "Support adding custom themes", - "dynamic_room_predecessors": "Dynamic room predecessors", - "dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)", - "element_call_video_rooms": "Element Call video rooms", - "exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages", - "exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.", - "experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", - "experimental_section": "Early previews", - "extended_profiles_msc_support": "Requires your server to support MSC4133", - "feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call", - "feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.", - "group_calls": "New group call experience", - "group_developer": "Developer", - "group_encryption": "Encryption", - "group_experimental": "Experimental", - "group_messaging": "Messaging", - "group_moderation": "Moderation", - "group_profile": "Profile", - "group_rooms": "Rooms", - "group_spaces": "Spaces", - "group_themes": "Themes", - "group_threads": "Threads", - "group_ui": "User interface", - "group_voip": "Voice & Video", - "group_widgets": "Widgets", - "hidebold": "Hide notification dot (only display counters badges)", - "html_topic": "Show HTML representation of room topics", - "join_beta": "Join the beta", - "join_beta_reload": "Joining the beta will reload %(brand)s.", - "jump_to_date": "Jump to date (adds /jumptodate and jump to date headers)", - "jump_to_date_msc_support": "Requires your server to support MSC3030", - "latex_maths": "Render LaTeX maths in messages", - "leave_beta": "Leave the beta", - "leave_beta_reload": "Leaving the beta will reload %(brand)s.", - "location_share_live": "Live Location Sharing", - "location_share_live_description": "Temporary implementation. Locations persist in room history.", - "mjolnir": "New ways to ignore people", - "msc3531_hide_messages_pending_moderation": "Let moderators hide messages pending moderation.", - "new_room_list": "Enable new room list", - "notification_settings": "New Notification Settings", - "notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.", - "notification_settings_beta_title": "Notification Settings", - "notifications": "Enable the notifications panel in the room header", - "release_announcement": "Release announcement", - "render_reaction_images": "Render custom images in reactions", - "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", - "report_to_moderators": "Report to moderators", - "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", - "sliding_sync": "Simplified Sliding Sync mode", - "sliding_sync_description": "Under active development, cannot be disabled.", - "sliding_sync_disabled_notice": "Log out and back in to disable", - "sliding_sync_server_no_support": "Your server lacks support", - "under_active_development": "Under active development.", - "unrealiable_e2e": "Unreliable in encrypted rooms", - "video_rooms": "Video rooms", - "video_rooms_a_new_way_to_chat": "A new way to chat over voice and video in %(brand)s.", - "video_rooms_always_on_voip_channels": "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.", - "video_rooms_beta": "Video rooms are a beta feature", - "video_rooms_faq1_answer": "Use the “+” button in the room section of the left panel.", - "video_rooms_faq1_question": "How can I create a video room?", - "video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.", - "video_rooms_faq2_question": "Can I use text chat alongside the video call?", - "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", - "wysiwyg_composer": "Rich text editor" + "dialog_title": "Settings: Labs" }, "labs_mjolnir": { - "advanced_warning": "⚠ These settings are meant for advanced users.", - "ban_reason": "Ignored/Blocked", - "error_adding_ignore": "Error adding ignored user/server", - "error_adding_list_description": "Please verify the room ID or address and try again.", - "error_adding_list_title": "Error subscribing to list", - "error_removing_ignore": "Error removing ignored user/server", - "error_removing_list_description": "Please try again or view your console for hints.", - "error_removing_list_title": "Error unsubscribing from list", - "explainer_1": "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", - "explainer_2": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", - "lists": "You are currently subscribed to:", - "lists_description_1": "Subscribing to a ban list will cause you to join it!", - "lists_description_2": "If this isn't what you want, please use a different tool to ignore users.", - "lists_heading": "Subscribed lists", - "lists_new_label": "Room ID or address of ban list", - "no_lists": "You are not subscribed to any lists", - "personal_description": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.", - "personal_empty": "You have not ignored anyone.", - "personal_heading": "Personal ban list", - "personal_new_label": "Server or user ID to ignore", - "personal_new_placeholder": "eg: @bot:* or example.org", - "personal_section": "You are currently ignoring:", - "room_name": "My Ban List", - "room_topic": "This is your list of users/servers you have blocked - don't leave the room!", - "rules_empty": "None", - "rules_server": "Server rules", - "rules_title": "Ban list rules - %(roomName)s", - "rules_user": "User rules", - "something_went_wrong": "Something went wrong. Please try again or view your console for hints.", - "title": "Ignored users", - "view_rules": "View rules" - }, - "language_dropdown_label": "Language Dropdown", - "leave_room_dialog": { - "last_person_warning": "You are the only person here. If you leave, no one will be able to join in the future, including you.", - "leave_room_question": "Are you sure you want to leave the room '%(roomName)s'?", - "leave_space_question": "Are you sure you want to leave the space '%(spaceName)s'?", - "room_leave_admin_warning": "You're the only administrator in this room. If you leave, nobody will be able to change room settings or take other important actions.", - "room_leave_mod_warning": "You're the only moderator in this room. If you leave, nobody will be able to change room settings or take other important actions.", - "room_rejoin_warning": "This room is not public. You will not be able to rejoin without an invite.", - "space_rejoin_warning": "This space is not public. You will not be able to rejoin without an invite." - }, - "left_panel": { - "open_dial_pad": "Open dial pad" - }, - "lightbox": { - "rotate_left": "Rotate Left", - "rotate_right": "Rotate Right", - "title": "Image view" - }, - "location_sharing": { - "MapStyleUrlNotConfigured": "This homeserver is not configured to display maps.", - "MapStyleUrlNotReachable": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", - "WebGLNotEnabled": "WebGL is required to display maps, please enable it in your browser settings.", - "click_drop_pin": "Click to drop a pin", - "click_move_pin": "Click to move the pin", - "close_sidebar": "Close sidebar", - "error_fetch_location": "Could not fetch location", - "error_no_perms_description": "You need to have the right permissions in order to share locations in this room.", - "error_no_perms_title": "You don't have permission to share locations", - "error_send_description": "%(brand)s could not send your location. Please try again later.", - "error_send_title": "We couldn't send your location", - "error_sharing_live_location": "An error occurred whilst sharing your live location", - "error_stopping_live_location": "An error occurred while stopping your live location", - "expand_map": "Expand map", - "failed_generic": "Failed to fetch your location. Please try again later.", - "failed_load_map": "Unable to load map", - "failed_permission": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", - "failed_timeout": "Timed out trying to fetch your location. Please try again later.", - "failed_unknown": "Unknown error fetching location. Please try again later.", - "find_my_location": "Find my location", - "live_description": "%(displayName)s's live location", - "live_enable_description": "Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.", - "live_enable_heading": "Live location sharing", - "live_location_active": "You are sharing your live location", - "live_location_enabled": "Live location enabled", - "live_location_ended": "Live location ended", - "live_location_error": "Live location error", - "live_locations_empty": "No live locations", - "live_share_button": "Share for %(duration)s", - "live_toggle_label": "Enable live location sharing", - "live_until": "Live until %(expiryTime)s", - "live_update_time": "Updated %(humanizedUpdateTime)s", - "loading_live_location": "Loading live location…", - "location_not_available": "Location not available", - "map_feedback": "Map feedback", - "mapbox_logo": "Mapbox logo", - "reset_bearing": "Reset bearing to north", - "share_button": "Share location", - "share_type_live": "My live location", - "share_type_own": "My current location", - "share_type_pin": "Drop a Pin", - "share_type_prompt": "What location type do you want to share?", - "toggle_attribution": "Toggle attribution" - }, - "member_list": { - "count": { - "one": "%(count)s Member", - "other": "%(count)s Members" - }, - "filter_placeholder": "Search room members", - "invite_button_no_perms_tooltip": "You do not have permission to invite users", - "invited_label": "Invited", - "no_matches": "No matches", - "power_label": "%(userName)s (power %(powerLevelNumber)s)" - }, - "member_list_back_action_label": "Room members", - "message_edit_dialog_title": "Message edits", - "migrating_crypto": "Hang tight. We are updating %(brand)s to make encryption faster and more reliable.", - "mobile_guide": { - "toast_accept": "Use app", - "toast_description": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", - "toast_title": "Use app for a better experience" - }, - "name_and_id": "%(name)s (%(userId)s)", - "no_more_results": "No more results", - "notif_panel": { - "empty_description": "You have no visible notifications.", - "empty_heading": "You're all caught up" + "dialog_title": "Settings: Ignored Users" }, "notifications": { - "all_messages": "All messages", - "all_messages_description": "Get notified for every message", - "class_global": "Global", - "class_other": "Other", - "default": "Default", - "email_pusher_app_display_name": "Email Notifications", - "enable_prompt_toast_description": "Enable desktop notifications", - "enable_prompt_toast_title": "Notifications", - "enable_prompt_toast_title_from_message_send": "Don't miss a reply", - "error_change_title": "Change notification settings", - "keyword": "Keyword", - "keyword_new": "New keyword", - "level_activity": "Activity", - "level_highlight": "Highlight", - "level_muted": "Muted", - "level_none": "None", - "level_notification": "Notification", - "level_unsent": "Unsent", - "mark_all_read": "Mark all as read", - "mentions_and_keywords": "@mentions & keywords", - "mentions_and_keywords_description": "Get notified only with mentions and keywords as set up in your settings", - "mentions_keywords": "Mentions & keywords", - "message_didnt_send": "Message didn't send. Click for info.", - "mute_description": "You won't get any notifications" - }, - "notifier": { - "m.key.verification.request": "%(name)s is requesting verification" - }, - "onboarding": { - "create_room": "Create a Group Chat", - "explore_rooms": "Explore Public Rooms", - "has_avatar_label": "Great, that'll help people know it's you", - "intro_byline": "Own your conversations.", - "intro_welcome": "Welcome to %(appName)s", - "no_avatar_label": "Add a photo so people know it's you.", - "send_dm": "Send a Direct Message", - "welcome_detail": "Now, let's help you get started", - "welcome_user": "Welcome %(name)s" - }, - "pill": { - "permalink_other_room": "Message in %(room)s", - "permalink_this_room": "Message from %(user)s" + "default_setting_description": "This setting will be applied by default to all your rooms.", + "default_setting_section": "I want to be notified for (Default Setting)", + "desktop_notification_message_preview": "Show message preview in desktop notification", + "dialog_title": "Settings: Notifications", + "email_description": "Receive an email summary of missed notifications", + "email_section": "Email summary", + "email_select": "Select which emails you want to send summaries to. Manage your emails in .", + "enable_audible_notifications_session": "Enable audible notifications for this session", + "enable_desktop_notifications_session": "Enable desktop notifications for this session", + "enable_email_notifications": "Enable email notifications for %(email)s", + "enable_notifications_account": "Enable notifications for this account", + "enable_notifications_account_detail": "Turn off to disable notifications on all your devices and sessions", + "enable_notifications_device": "Enable notifications for this device", + "error_loading": "There was an error loading your notification settings.", + "error_permissions_denied": "%(brand)s does not have permission to send you notifications - please check your browser settings", + "error_permissions_missing": "%(brand)s was not given permission to send notifications - please try again", + "error_saving": "Error saving notification preferences", + "error_saving_detail": "An error occurred whilst saving your notification preferences.", + "error_title": "Unable to enable Notifications", + "error_updating": "An error occurred when updating your notification preferences. Please try to toggle your option again.", + "invites": "Invited to a room", + "keywords": "Show a badge when keywords are used in a room.", + "keywords_prompt": "Enter keywords here, or use for spelling variations or nicknames", + "labs_notice_prompt": "Update:We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active. If you proceed, some of your settings may change. Learn more", + "mentions_keywords": "Mentions and Keywords", + "mentions_keywords_only": "Mentions and Keywords only", + "messages_containing_keywords": "Messages containing keywords", + "noisy": "Noisy", + "notices": "Messages sent by bots", + "notify_at_room": "Notify when someone mentions using @room", + "notify_keyword": "Notify when someone uses a keyword", + "notify_mention": "Notify when someone mentions using @displayname or %(mxid)s", + "other_section": "Other things we think you might be interested in:", + "people_mentions_keywords": "People, Mentions and Keywords", + "play_sound_for_description": "Applied by default to all rooms on all devices.", + "play_sound_for_section": "Play a sound for", + "push_targets": "Notification targets", + "quick_actions_mark_all_read": "Mark all messages as read", + "quick_actions_reset": "Reset to default settings", + "quick_actions_section": "Quick Actions", + "room_activity": "New room activity, upgrades and status messages occur", + "rule_call": "Call invitation", + "rule_contains_display_name": "Messages containing my display name", + "rule_contains_user_name": "Messages containing my username", + "rule_encrypted": "Encrypted messages in group chats", + "rule_encrypted_room_one_to_one": "Encrypted messages in one-to-one chats", + "rule_invite_for_me": "When I'm invited to a room", + "rule_message": "Messages in group chats", + "rule_room_one_to_one": "Messages in one-to-one chats", + "rule_roomnotif": "Messages containing @room", + "rule_suppress_notices": "Messages sent by bot", + "rule_tombstone": "When rooms are upgraded", + "show_message_desktop_notification": "Show message in desktop notification", + "voip": "Audio and Video calls" }, - "poll": { - "create_poll_action": "Create Poll", - "create_poll_title": "Create poll", - "disclosed_notes": "Voters see results as soon as they have voted", - "edit_poll_title": "Edit poll", - "end_description": "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.", - "end_message": "The poll has ended. Top answer: %(topAnswer)s", - "end_message_no_votes": "The poll has ended. No votes were cast.", - "end_title": "End Poll", - "error_ending_description": "Sorry, the poll did not end. Please try again.", - "error_ending_title": "Failed to end poll", - "error_voting_description": "Sorry, your vote was not registered. Please try again.", - "error_voting_title": "Vote not registered", - "failed_send_poll_description": "Sorry, the poll you tried to create was not posted.", - "failed_send_poll_title": "Failed to post poll", - "notes": "Results are only revealed when you end the poll", - "options_add_button": "Add option", - "options_heading": "Create options", - "options_label": "Option %(number)s", - "options_placeholder": "Write an option", - "topic_heading": "What is your poll question or topic?", - "topic_label": "Question or topic", - "topic_placeholder": "Write something…", - "total_decryption_errors": "Due to decryption errors, some votes may not be counted", - "total_n_votes": { - "one": "%(count)s vote cast. Vote to see the results", - "other": "%(count)s votes cast. Vote to see the results" - }, - "total_n_votes_voted": { - "one": "Based on %(count)s vote", - "other": "Based on %(count)s votes" - }, - "total_no_votes": "No votes cast", - "total_not_ended": "Results will be visible when the poll is ended", - "type_closed": "Closed poll", - "type_heading": "Poll type", - "type_open": "Open poll", - "unable_edit_description": "Sorry, you can't edit a poll after votes have been cast.", - "unable_edit_title": "Can't edit poll" - }, - "power_level": { - "admin": "Admin", - "custom": "Custom (%(level)s)", - "custom_level": "Custom level", - "default": "Default", - "label": "Power level", - "moderator": "Moderator", - "restricted": "Restricted" - }, - "powered_by_matrix": "Powered by Matrix", - "powered_by_matrix_with_logo": "Decentralised, encrypted chat & collaboration powered by $matrixLogo", - "presence": { - "away": "Away", - "busy": "Busy", - "idle": "Idle", - "idle_for": "Idle for %(duration)s", - "offline": "Offline", - "offline_for": "Offline for %(duration)s", - "online": "Online", - "online_for": "Online for %(duration)s", - "unknown": "Unknown", - "unknown_for": "Unknown for %(duration)s", - "unreachable": "User's server unreachable" - }, - "quick_settings": { - "all_settings": "All settings", - "metaspace_section": "Pin to sidebar", - "sidebar_settings": "More options", - "title": "Quick settings" - }, - "quit_warning": { - "call_in_progress": "You seem to be in a call, are you sure you want to quit?", - "file_upload_in_progress": "You seem to be uploading files, are you sure you want to quit?" + "preferences": { + "Electron.enableHardwareAcceleration": "Enable hardware acceleration (restart %(appName)s to take effect)", + "always_show_menu_bar": "Always show the window menu bar", + "autocomplete_delay": "Autocomplete delay (ms)", + "code_blocks_heading": "Code blocks", + "compact_modern": "Use a more compact 'Modern' layout", + "composer_heading": "Composer", + "default_timezone": "Browser default (%(timezone)s)", + "dialog_title": "Settings: Preferences", + "enable_hardware_acceleration": "Enable hardware acceleration", + "enable_tray_icon": "Show tray icon and minimise window to it on close", + "keyboard_heading": "Keyboard shortcuts", + "keyboard_view_shortcuts_button": "To view all keyboard shortcuts, click here.", + "media_heading": "Images, GIFs and videos", + "presence_description": "Share your activity and status with others.", + "publish_timezone": "Publish timezone on public profile", + "rm_lifetime": "Read Marker lifetime (ms)", + "rm_lifetime_offscreen": "Read Marker off-screen lifetime (ms)", + "room_directory_heading": "Room directory", + "room_list_heading": "Room list", + "show_avatars_pills": "Show avatars in user, room and event mentions", + "show_polls_button": "Show polls button", + "surround_text": "Surround selected text when typing special characters", + "time_heading": "Displaying time", + "user_timezone": "Set timezone" }, - "redact": { - "confirm_button": "Confirm Removal", - "confirm_description": "Are you sure you wish to remove (delete) this event?", - "confirm_description_state": "Note that removing room changes like this could undo the change.", - "error": "You cannot delete this message. (%(code)s)", - "ongoing": "Removing…", - "reason_label": "Reason (optional)" - }, - "reject_invitation_dialog": { - "confirmation": "Are you sure you want to reject the invitation?", - "failed": "Failed to reject invitation", - "title": "Reject invitation" - }, - "report_content": { - "description": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.", - "disagree": "Disagree", - "error_create_room_moderation_bot": "Unable to create room with moderation bot", - "hide_messages_from_user": "Check if you want to hide all current and future messages from this user.", - "ignore_user": "Ignore user", - "illegal_content": "Illegal Content", - "missing_reason": "Please fill why you're reporting.", - "nature": "Please pick a nature and describe what makes this message abusive.", - "nature_disagreement": "What this user is writing is wrong.\nThis will be reported to the room moderators.", - "nature_illegal": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", - "nature_nonstandard_admin": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.", - "nature_nonstandard_admin_encrypted": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", - "nature_other": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", - "nature_spam": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", - "nature_toxic": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", - "other_label": "Other", - "report_content_to_homeserver": "Report Content to Your Homeserver Administrator", - "report_entire_room": "Report the entire room", - "spam_or_propaganda": "Spam or propaganda", - "toxic_behaviour": "Toxic Behaviour" - }, - "restore_key_backup_dialog": { - "count_of_decryption_failures": "Failed to decrypt %(failedCount)s sessions!", - "count_of_successfully_restored_keys": "Successfully restored %(sessionCount)s keys", - "enter_key_description": "Access your secure message history and set up secure messaging by entering your Recovery Key.", - "enter_key_title": "Enter Recovery Key", - "enter_phrase_description": "Access your secure message history and set up secure messaging by entering your Security Phrase.", - "enter_phrase_title": "Enter Security Phrase", - "incorrect_security_phrase_dialog": "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.", - "incorrect_security_phrase_title": "Incorrect Security Phrase", - "key_backup_warning": "Warning: you should only set up key backup from a trusted computer.", - "key_fetch_in_progress": "Fetching keys from server…", - "key_forgotten_text": "If you've forgotten your Recovery Key you can ", - "key_is_invalid": "Not a valid Recovery Key", - "key_is_valid": "This looks like a valid Recovery Key!", - "keys_restored_title": "Keys restored", - "load_error_content": "Unable to load backup status", - "load_keys_progress": "%(completed)s of %(total)s keys restored", - "no_backup_error": "No backup found!", - "phrase_forgotten_text": "If you've forgotten your Security Phrase you can use your Recovery Key or set up new recovery options", - "recovery_key_mismatch_description": "Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.", - "recovery_key_mismatch_title": "Recovery Key mismatch", - "restore_failed_error": "Unable to restore backup" - }, - "right_panel": { - "add_integrations": "Add extensions", - "add_topic": "Add topic", - "extensions_button": "Extensions", - "extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room", - "extensions_empty_title": "Boost productivity with more tools, widgets and bots", - "files_button": "Files", - "pinned_messages": { - "empty_description": "Select a message and choose “%(pinAction)s” to it include here.", - "empty_title": "Pin important messages so that they can be easily discovered", - "header": { - "one": "1 Pinned message", - "other": "%(count)s Pinned messages", - "zero": "Pinned messages" - }, - "limits": { - "other": "You can only pin up to %(count)s widgets" - }, - "menu": "Open menu", - "release_announcement": { - "close": "Ok", - "description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.", - "title": "All new pinned messages" - }, - "reply_thread": "Reply to a thread message", - "unpin_all": { - "button": "Unpin all messages", - "content": "Make sure that you really want to remove all pinned messages. This action can’t be undone.", - "title": "Unpin all messages?" - }, - "view": "View in timeline" - }, - "pinned_messages_button": "Pinned messages", - "poll": { - "active_heading": "Active polls", - "empty_active": "There are no active polls in this room", - "empty_active_load_more": "There are no active polls. Load more polls to view polls for previous months", - "empty_active_load_more_n_days": { - "one": "There are no active polls for the past day. Load more polls to view polls for previous months", - "other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months" - }, - "empty_past": "There are no past polls in this room", - "empty_past_load_more": "There are no past polls. Load more polls to view polls for previous months", - "empty_past_load_more_n_days": { - "one": "There are no past polls for the past day. Load more polls to view polls for previous months", - "other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months" - }, - "final_result": { - "one": "Final result based on %(count)s vote", - "other": "Final result based on %(count)s votes" - }, - "load_more": "Load more polls", - "loading": "Loading polls", - "past_heading": "Past polls", - "view_in_timeline": "View poll in timeline", - "view_poll": "View poll" - }, - "polls_button": "Polls", - "room_summary_card": { - "title": "Room info" - }, - "thread_list": { - "context_menu_label": "Thread options" - }, - "video_room_chat": { - "title": "Chat" - } - }, - "room": { - "3pid_invite_email_not_found_account": "This invite was sent to %(email)s which is not associated with your account", - "3pid_invite_email_not_found_account_room": "This invite to %(roomName)s was sent to %(email)s which is not associated with your account", - "3pid_invite_error_description": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.", - "3pid_invite_error_invite_action": "Try to join anyway", - "3pid_invite_error_invite_subtitle": "You can only join it with a working invite.", - "3pid_invite_error_public_subtitle": "You can still join here.", - "3pid_invite_error_title": "Something went wrong with your invite.", - "3pid_invite_error_title_room": "Something went wrong with your invite to %(roomName)s", - "3pid_invite_no_is_subtitle": "Use an identity server in Settings to receive invites directly in %(brand)s.", - "banned_by": "You were banned by %(memberName)s", - "banned_from_room_by": "You were banned from %(roomName)s by %(memberName)s", - "context_menu": { - "copy_link": "Copy room link", - "favourite": "Favourite", - "forget": "Forget Room", - "low_priority": "Low Priority", - "mark_read": "Mark as read", - "mark_unread": "Mark as unread", - "notifications_default": "Match default setting", - "notifications_mute": "Mute room", - "title": "Room options", - "unfavourite": "Favourited" - }, - "creating_room_text": "We're creating a room with %(names)s", - "dm_invite_action": "Start chatting", - "dm_invite_subtitle": " wants to chat", - "dm_invite_title": "Do you want to chat with %(user)s?", - "drop_file_prompt": "Drop file here to upload", - "edit_topic": "Edit topic", - "error_3pid_invite_email_lookup": "Unable to find user by email", - "error_cancel_knock_title": "Failed to cancel", - "error_join_403": "You need an invite to access this room.", - "error_join_404_1": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.", - "error_join_404_2": "If you know a room address, try joining through that instead.", - "error_join_404_invite": "The person who invited you has already left, or their server is offline.", - "error_join_404_invite_same_hs": "The person who invited you has already left.", - "error_join_connection": "There was an error joining.", - "error_join_incompatible_version_1": "Sorry, your homeserver is too old to participate here.", - "error_join_incompatible_version_2": "Please contact your homeserver administrator.", - "error_join_title": "Failed to join", - "error_jump_to_date": "Server returned %(statusCode)s with error code %(errorCode)s", - "error_jump_to_date_connection": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", - "error_jump_to_date_details": "Error details", - "error_jump_to_date_not_found": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", - "error_jump_to_date_send_logs_prompt": "Please submit debug logs to help us track down the problem.", - "error_jump_to_date_title": "Unable to find event at that date", - "face_pile_summary": { - "one": "%(count)s person you know has already joined", - "other": "%(count)s people you know have already joined" - }, - "face_pile_tooltip_label": { - "one": "View 1 member", - "other": "View all %(count)s members" - }, - "face_pile_tooltip_shortcut": "Including %(commaSeparatedMembers)s", - "face_pile_tooltip_shortcut_joined": "Including you, %(commaSeparatedMembers)s", - "failed_reject_invite": "Failed to reject invite", - "forget_room": "Forget this room", - "forget_space": "Forget this space", - "header": { - "n_people_asking_to_join": { - "one": "Asking to join", - "other": "%(count)s people asking to join" - }, - "room_is_public": "This room is public" - }, - "header_avatar_open_settings_label": "Open room settings", - "header_face_pile_tooltip": "People", - "header_untrusted_label": "Untrusted", - "inaccessible": "This room or space is not accessible at this time.", - "inaccessible_name": "%(roomName)s is not accessible at this time.", - "inaccessible_subtitle_1": "Try again later, or ask a room or space admin to check if you have access.", - "inaccessible_subtitle_2": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.", - "intro": { - "dm_caption": "Only the two of you are in this conversation, unless either of you invites anyone to join.", - "enable_encryption_prompt": "Enable encryption in settings.", - "encrypted_3pid_dm_pending_join": "Once everyone has joined, you’ll be able to chat", - "no_avatar_label": "Add a photo, so people can easily spot your room.", - "no_topic": "Add a topic to help people know what it is about.", - "private_unencrypted_warning": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", - "room_invite": "Invite to just this room", - "send_message_start_dm": "Send your first message to invite to chat", - "start_of_dm_history": "This is the beginning of your direct message history with .", - "start_of_room": "This is the start of .", - "topic": "Topic: %(topic)s ", - "topic_edit": "Topic: %(topic)s (edit)", - "unencrypted_warning": "End-to-end encryption isn't enabled", - "user_created": "%(displayName)s created this room.", - "you_created": "You created this room." - }, - "invite_email_mismatch_suggestion": "Share this email in Settings to receive invites directly in %(brand)s.", - "invite_reject_ignore": "Reject & Ignore user", - "invite_sent_to_email": "This invite was sent to %(email)s", - "invite_sent_to_email_room": "This invite to %(roomName)s was sent to %(email)s", - "invite_subtitle": "Invited by ", - "invite_this_room": "Invite to this room", - "invite_title": "Do you want to join %(roomName)s?", - "inviter_unknown": "Unknown", - "invites_you_text": " invites you", - "join_button_account": "Sign Up", - "join_failed_needs_invite": "To view %(roomName)s, you need an invite", - "join_the_discussion": "Join the discussion", - "join_title": "Join the room to participate", - "join_title_account": "Join the conversation with an account", - "joining": "Joining…", - "joining_room": "Joining room…", - "joining_space": "Joining space…", - "jump_read_marker": "Jump to first unread message.", - "jump_to_bottom_button": "Scroll to most recent messages", - "jump_to_date": "Jump to date", - "jump_to_date_beginning": "The beginning of the room", - "jump_to_date_prompt": "Pick a date to jump to", - "kick_reason": "Reason: %(reason)s", - "kicked_by": "You were removed by %(memberName)s", - "kicked_from_room_by": "You were removed from %(roomName)s by %(memberName)s", - "knock_cancel_action": "Cancel request", - "knock_denied_subtitle": "As you have been denied access, you cannot rejoin unless you are invited by the admin or moderator of the group.", - "knock_denied_title": "You have been denied access", - "knock_message_field_placeholder": "Message (optional)", - "knock_prompt": "Ask to join?", - "knock_prompt_name": "Ask to join %(roomName)s?", - "knock_send_action": "Request access", - "knock_sent": "Request to join sent", - "knock_sent_subtitle": "Your request to join is pending.", - "knock_subtitle": "You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.", - "leave_error_title": "Error leaving room", - "leave_server_notices_description": "This room is used for important messages from the Homeserver, so you cannot leave it.", - "leave_server_notices_title": "Can't leave Server Notices room", - "leave_unexpected_error": "Unexpected server error trying to leave the room", - "link_email_to_receive_3pid_invite": "Link this email with your account in Settings to receive invites directly in %(brand)s.", - "loading_preview": "Loading preview", - "no_peek_join_prompt": "%(roomName)s can't be previewed. Do you want to join it?", - "no_peek_no_name_join_prompt": "There's no preview, would you like to join?", - "not_found_subtitle": "Are you sure you're at the right place?", - "not_found_title": "This room or space does not exist.", - "not_found_title_name": "%(roomName)s does not exist.", - "peek_join_prompt": "You're previewing %(roomName)s. Want to join it?", - "pinned_message_badge": "Pinned message", - "pinned_message_banner": { - "button_close_list": "Close list", - "button_view_all": "View all", - "description": "This room has pinned messages. Click to view them.", - "go_to_message": "View the pinned message in the timeline.", - "title": "%(index)s of %(length)s Pinned messages" - }, - "read_topic": "Click to read topic", - "rejecting": "Rejecting invite…", - "rejoin_button": "Re-join", - "search": { - "all_rooms_button": "Search all rooms", - "placeholder": "Search messages…", - "summary": { - "one": "1 result found for “”", - "other": "%(count)s results found for “”" - }, - "this_room_button": "Search this room" - }, - "status_bar": { - "delete_all": "Delete all", - "exceeded_resource_limit": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", - "homeserver_blocked": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", - "monthly_user_limit_reached": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", - "requires_consent_agreement": "You can't send any messages until you review and agree to our terms and conditions.", - "retry_all": "Retry all", - "select_messages_to_retry": "You can select all or individual messages to retry or delete", - "server_connectivity_lost_description": "Sent messages will be stored until your connection has returned.", - "server_connectivity_lost_title": "Connectivity to the server has been lost.", - "some_messages_not_sent": "Some of your messages have not been sent" - }, - "unknown_status_code_for_timeline_jump": "unknown status code", - "unread_notifications_predecessor": { - "one": "You have %(count)s unread notification in a prior version of this room.", - "other": "You have %(count)s unread notifications in a prior version of this room." - }, - "upgrade_error_description": "Double check that your server supports the room version chosen and try again.", - "upgrade_error_title": "Error upgrading room", - "upgrade_warning_bar": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", - "upgrade_warning_bar_admins": "Only room administrators will see this warning", - "upgrade_warning_bar_unstable": "This room is running room version , which this homeserver has marked as unstable.", - "upgrade_warning_bar_upgraded": "This room has already been upgraded.", - "upload": { - "uploading_multiple_file": { - "one": "Uploading %(filename)s and %(count)s other", - "other": "Uploading %(filename)s and %(count)s others" - }, - "uploading_single_file": "Uploading %(filename)s" - }, - "waiting_for_join_subtitle": "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", - "waiting_for_join_title": "Waiting for users to join %(brand)s" - }, - "room_list": { - "add_room_label": "Add room", - "add_space_label": "Add space", - "breadcrumbs_empty": "No recently visited rooms", - "breadcrumbs_label": "Recently visited rooms", - "failed_add_tag": "Failed to add tag %(tagName)s to room", - "failed_remove_tag": "Failed to remove tag %(tagName)s from room", - "failed_set_dm_tag": "Failed to set direct message tag", - "home_menu_label": "Home options", - "join_public_room_label": "Join public room", - "joining_rooms_status": { - "one": "Currently joining %(count)s room", - "other": "Currently joining %(count)s rooms" - }, - "notification_options": "Notification options", - "redacting_messages_status": { - "one": "Currently removing messages in %(count)s room", - "other": "Currently removing messages in %(count)s rooms" - }, - "show_less": "Show less", - "show_n_more": { - "one": "Show %(count)s more", - "other": "Show %(count)s more" - }, - "show_previews": "Show previews of messages", - "sort_by": "Sort by", - "sort_by_activity": "Activity", - "sort_by_alphabet": "A-Z", - "sort_unread_first": "Show rooms with unread messages first", - "space_menu_label": "%(spaceName)s menu", - "sublist_options": "List options", - "suggested_rooms_heading": "Suggested Rooms" - }, - "room_settings": { - "access": { - "description_space": "Decide who can view and join %(spaceName)s.", - "title": "Access" - }, - "advanced": { - "error_upgrade_description": "The room upgrade could not be completed", - "error_upgrade_title": "Failed to upgrade room", - "information_section_room": "Room information", - "information_section_space": "Space information", - "room_id": "Internal room ID", - "room_predecessor": "View older messages in %(roomName)s.", - "room_upgrade_button": "Upgrade this room to the recommended room version", - "room_upgrade_warning": "Warning: upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.", - "room_version": "Room version:", - "room_version_section": "Room version", - "space_predecessor": "View older version of %(spaceName)s.", - "space_upgrade_button": "Upgrade this space to the recommended room version", - "unfederated": "This room is not accessible by remote Matrix servers", - "upgrade_button": "Upgrade this room to version %(version)s", - "upgrade_dialog_description": "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:", - "upgrade_dialog_description_1": "Create a new room with the same name, description and avatar", - "upgrade_dialog_description_2": "Update any local room aliases to point to the new room", - "upgrade_dialog_description_3": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", - "upgrade_dialog_description_4": "Put a link back to the old room at the start of the new room so people can see old messages", - "upgrade_dialog_title": "Upgrade Room Version", - "upgrade_dwarning_ialog_title_public": "Upgrade public room", - "upgrade_warning_dialog_description": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", - "upgrade_warning_dialog_explainer": "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.", - "upgrade_warning_dialog_footer": "You'll upgrade this room from to .", - "upgrade_warning_dialog_invite_label": "Automatically invite members from this room to the new one", - "upgrade_warning_dialog_report_bug_prompt": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", - "upgrade_warning_dialog_report_bug_prompt_link": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", - "upgrade_warning_dialog_title": "Upgrade room", - "upgrade_warning_dialog_title_private": "Upgrade private room" - }, - "alias_not_specified": "not specified", - "bridges": { - "description": "This room is bridging messages to the following platforms. Learn more.", - "empty": "This room isn't bridging messages to any platforms. Learn more.", - "title": "Bridges" - }, - "delete_avatar_label": "Delete avatar", - "general": { - "alias_field_has_domain_invalid": "Missing domain separator e.g. (:domain.org)", - "alias_field_has_localpart_invalid": "Missing room name or separator e.g. (my-room:domain.org)", - "alias_field_matches_invalid": "This address does not point at this room", - "alias_field_placeholder_default": "e.g. my-room", - "alias_field_required_invalid": "Please provide an address", - "alias_field_safe_localpart_invalid": "Some characters not allowed", - "alias_field_taken_invalid": "This address had invalid server or is already in use", - "alias_field_taken_invalid_domain": "This address is already in use", - "alias_field_taken_valid": "This address is available to use", - "alias_heading": "Room address", - "aliases_items_label": "Other published addresses:", - "aliases_no_items_label": "No other published addresses yet, add one below", - "aliases_section": "Room Addresses", - "avatar_field_label": "Room avatar", - "canonical_alias_field_label": "Main address", - "default_url_previews_off": "URL previews are disabled by default for participants in this room.", - "default_url_previews_on": "URL previews are enabled by default for participants in this room.", - "description_space": "Edit settings relating to your space.", - "error_creating_alias_description": "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.", - "error_creating_alias_title": "Error creating address", - "error_deleting_alias_description": "There was an error removing that address. It may no longer exist or a temporary error occurred.", - "error_deleting_alias_description_forbidden": "You don't have permission to delete the address.", - "error_deleting_alias_title": "Error removing address", - "error_publishing": "Unable to publish room", - "error_publishing_detail": "There was an error publishing this room", - "error_save_space_settings": "Failed to save space settings.", - "error_updating_alias_description": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", - "error_updating_canonical_alias_description": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", - "error_updating_canonical_alias_title": "Error updating main address", - "leave_space": "Leave Space", - "local_alias_field_label": "Local address", - "local_aliases_explainer_room": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", - "local_aliases_explainer_space": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", - "local_aliases_section": "Local Addresses", - "name_field_label": "Room Name", - "new_alias_placeholder": "New published address (e.g. #alias:server)", - "no_aliases_room": "This room has no local addresses", - "no_aliases_space": "This space has no local addresses", - "other_section": "Other", - "publish_toggle": "Publish this room to the public in %(domain)s's room directory?", - "published_aliases_description": "To publish an address, it needs to be set as a local address first.", - "published_aliases_explainer_room": "Published addresses can be used by anyone on any server to join your room.", - "published_aliases_explainer_space": "Published addresses can be used by anyone on any server to join your space.", - "published_aliases_section": "Published Addresses", - "save": "Save Changes", - "topic_field_label": "Room Topic", - "url_preview_encryption_warning": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", - "url_preview_explainer": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", - "url_previews_section": "URL Previews", - "user_url_previews_default_off": "You have disabled URL previews by default.", - "user_url_previews_default_on": "You have enabled URL previews by default." - }, - "notifications": { - "browse_button": "Browse", - "custom_sound_prompt": "Set a new custom sound", - "notification_sound": "Notification sound", - "settings_link": "Get notifications as set up in your settings", - "sounds_section": "Sounds", - "upload_sound_label": "Upload custom sound", - "uploaded_sound": "Uploaded sound" - }, - "people": { - "knock_empty": "No requests", - "knock_section": "Asking to join", - "see_less": "See less", - "see_more": "See more" - }, - "permissions": { - "add_privileged_user_description": "Give one or multiple users in this room more privileges", - "add_privileged_user_filter_placeholder": "Search users in this room…", - "add_privileged_user_heading": "Add privileged users", - "ban": "Ban users", - "ban_reason": "Reason", - "banned_by": "Banned by %(displayName)s", - "banned_users_section": "Banned users", - "error_changing_pl_description": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.", - "error_changing_pl_reqs_description": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", - "error_changing_pl_reqs_title": "Error changing power level requirement", - "error_changing_pl_title": "Error changing power level", - "error_unbanning": "Failed to unban", - "events_default": "Send messages", - "invite": "Invite users", - "kick": "Remove users", - "m.call": "Start %(brand)s calls", - "m.call.member": "Join %(brand)s calls", - "m.reaction": "Send reactions", - "m.room.avatar": "Change room avatar", - "m.room.avatar_space": "Change space avatar", - "m.room.canonical_alias": "Change main address for the room", - "m.room.canonical_alias_space": "Change main address for the space", - "m.room.encryption": "Enable room encryption", - "m.room.history_visibility": "Change history visibility", - "m.room.name": "Change room name", - "m.room.name_space": "Change space name", - "m.room.pinned_events": "Manage pinned events", - "m.room.power_levels": "Change permissions", - "m.room.redaction": "Remove messages sent by me", - "m.room.server_acl": "Change server ACLs", - "m.room.tombstone": "Upgrade the room", - "m.room.topic": "Change topic", - "m.room.topic_space": "Change description", - "m.space.child": "Manage rooms in this space", - "m.widget": "Modify widgets", - "muted_users_section": "Muted Users", - "no_privileged_users": "No users have specific privileges in this room", - "notifications.room": "Notify everyone", - "permissions_section": "Permissions", - "permissions_section_description_room": "Select the roles required to change various parts of the room", - "permissions_section_description_space": "Select the roles required to change various parts of the space", - "privileged_users_section": "Privileged Users", - "redact": "Remove messages sent by others", - "send_event_type": "Send %(eventType)s events", - "state_default": "Change settings", - "title": "Roles & Permissions", - "users_default": "Default role" - }, - "security": { - "enable_encryption_confirm_description": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", - "enable_encryption_confirm_title": "Enable encryption?", - "enable_encryption_public_room_confirm_description_1": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", - "enable_encryption_public_room_confirm_description_2": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", - "enable_encryption_public_room_confirm_title": "Are you sure you want to add encryption to this public room?", - "encrypted_room_public_confirm_description_1": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.", - "encrypted_room_public_confirm_description_2": "To avoid these issues, create a new public room for the conversation you plan to have.", - "encrypted_room_public_confirm_title": "Are you sure you want to make this encrypted room public?", - "encryption_forced": "Your server requires encryption to be disabled.", - "encryption_permanent": "Once enabled, encryption cannot be disabled.", - "error_join_rule_change_title": "Failed to update the join rules", - "error_join_rule_change_unknown": "Unknown failure", - "guest_access_warning": "People with supported clients will be able to join the room without having a registered account.", - "history_visibility_invited": "Members only (since they were invited)", - "history_visibility_joined": "Members only (since they joined)", - "history_visibility_legend": "Who can read history?", - "history_visibility_shared": "Members only (since the point in time of selecting this option)", - "history_visibility_warning": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.", - "history_visibility_world_readable": "Anyone", - "join_rule_description": "Decide who can join %(roomName)s.", - "join_rule_invite": "Private (invite only)", - "join_rule_invite_description": "Only invited people can join.", - "join_rule_knock": "Ask to join", - "join_rule_knock_description": "People cannot join unless access is granted.", - "join_rule_public_description": "Anyone can find and join.", - "join_rule_restricted": "Space members", - "join_rule_restricted_description": "Anyone in a space can find and join. Edit which spaces can access here.", - "join_rule_restricted_description_active_space": "Anyone in can find and join. You can select other spaces too.", - "join_rule_restricted_description_prompt": "Anyone in a space can find and join. You can select multiple spaces.", - "join_rule_restricted_description_spaces": "Spaces with access", - "join_rule_restricted_dialog_description": "Decide which spaces can access this room. If a space is selected, its members can find and join .", - "join_rule_restricted_dialog_empty_warning": "You're removing all spaces. Access will default to invite only", - "join_rule_restricted_dialog_filter_placeholder": "Search spaces", - "join_rule_restricted_dialog_heading_known": "Other spaces you know", - "join_rule_restricted_dialog_heading_other": "Other spaces or rooms you might not know", - "join_rule_restricted_dialog_heading_room": "Spaces you know that contain this room", - "join_rule_restricted_dialog_heading_space": "Spaces you know that contain this space", - "join_rule_restricted_dialog_heading_unknown": "These are likely ones other room admins are a part of.", - "join_rule_restricted_dialog_title": "Select spaces", - "join_rule_restricted_n_more": { - "one": "& %(count)s more", - "other": "& %(count)s more" - }, - "join_rule_restricted_summary": { - "one": "Currently, a space has access", - "other": "Currently, %(count)s spaces have access" - }, - "join_rule_restricted_upgrade_description": "This upgrade will allow members of selected spaces access to this room without an invite.", - "join_rule_restricted_upgrade_warning": "This room is in some spaces you're not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.", - "join_rule_upgrade_awaiting_room": "Loading new room", - "join_rule_upgrade_required": "Upgrade required", - "join_rule_upgrade_sending_invites": { - "one": "Sending invite...", - "other": "Sending invites... (%(progress)s out of %(count)s)" - }, - "join_rule_upgrade_updating_spaces": { - "one": "Updating space...", - "other": "Updating spaces... (%(progress)s out of %(count)s)" - }, - "join_rule_upgrade_upgrading_room": "Upgrading room", - "public_without_alias_warning": "To link to this room, please add an address.", - "publish_room": "Make this room visible in the public room directory.", - "publish_space": "Make this space visible in the public room directory.", - "strict_encryption": "Never send encrypted messages to unverified sessions in this room from this session", - "title": "Security & Privacy" - }, - "title": "Room Settings - %(roomName)s", - "upload_avatar_label": "Upload avatar", - "visibility": { - "alias_section": "Address", - "error_failed_save": "Failed to update the visibility of this space", - "error_update_guest_access": "Failed to update the guest access of this space", - "error_update_history_visibility": "Failed to update the history visibility of this space", - "guest_access_explainer": "Guests can join a space without having an account.", - "guest_access_explainer_public_space": "This may be useful for public spaces.", - "guest_access_label": "Enable guest access", - "history_visibility_anyone_space": "Preview Space", - "history_visibility_anyone_space_description": "Allow people to preview your space before they join.", - "history_visibility_anyone_space_recommendation": "Recommended for public spaces.", - "title": "Visibility" - }, - "voip": { - "call_type_section": "Call type", - "enable_element_call_caption": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", - "enable_element_call_label": "Enable %(brand)s as an additional calling option in this room", - "enable_element_call_no_permissions_tooltip": "You do not have sufficient permissions to change this." - } - }, - "room_summary_card_back_action_label": "Room info", - "scalar": { - "error_create": "Unable to create widget.", - "error_membership": "You are not in this room.", - "error_missing_room_id": "Missing roomId.", - "error_missing_room_id_request": "Missing room_id in request", - "error_missing_user_id_request": "Missing user_id in request", - "error_permission": "You do not have permission to do that in this room.", - "error_power_level_invalid": "Power level must be positive integer.", - "error_room_not_visible": "Room %(roomId)s not visible", - "error_room_unknown": "This room is not recognised.", - "error_send_request": "Failed to send request.", - "failed_read_event": "Failed to read events", - "failed_send_event": "Failed to send event" - }, - "server_offline": { - "description": "Your server isn't responding to some of your requests. Below are some of the most likely reasons.", - "description_1": "The server (%(serverName)s) took too long to respond.", - "description_2": "Your firewall or anti-virus is blocking the request.", - "description_3": "A browser extension is preventing the request.", - "description_4": "The server is offline.", - "description_5": "The server has denied your request.", - "description_6": "Your area is experiencing difficulties connecting to the internet.", - "description_7": "A connection error occurred while trying to contact the server.", - "description_8": "The server is not configured to indicate what the problem is (CORS).", - "empty_timeline": "You're all caught up.", - "recent_changes_heading": "Recent changes that have not yet been received", - "title": "Server isn't responding" - }, - "seshat": { - "error_initialising": "Message search initialisation failed, check your settings for more information", - "reset_button": "Reset event store", - "reset_description": "You most likely do not want to reset your event index store", - "reset_explainer": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated", - "reset_title": "Reset event store?", - "warning_kind_files": "This version of %(brand)s does not support viewing some encrypted files", - "warning_kind_files_app": "Use the Desktop app to see all encrypted files", - "warning_kind_search": "This version of %(brand)s does not support searching encrypted messages", - "warning_kind_search_app": "Use the Desktop app to search encrypted messages" - }, - "setting": { - "help_about": { - "access_token_detail": "Your access token gives full access to your account. Do not share it with anyone.", - "brand_version": "%(brand)s version:", - "clear_cache_reload": "Clear cache and reload", - "crypto_version": "Crypto version:", - "dialog_title": "Settings: Help & About", - "help_link": "For help with using %(brand)s, click here.", - "homeserver": "Homeserver is %(homeserverUrl)s", - "identity_server": "Identity server is %(identityServerUrl)s", - "title": "Help & About", - "versions": "Versions" - } - }, - "settings": { - "account": { - "dialog_title": "Settings: Account", - "title": "Account" - }, - "all_rooms_home": "Show all rooms in Home", - "all_rooms_home_description": "All rooms you're in will appear in Home.", - "always_show_message_timestamps": "Always show message timestamps", - "appearance": { - "bundled_emoji_font": "Use bundled emoji font", - "compact_layout": "Show compact text and messages", - "compact_layout_description": "Modern layout must be selected to use this feature.", - "custom_font": "Use a system font", - "custom_font_description": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", - "custom_font_name": "System font name", - "custom_font_size": "Use custom size", - "custom_theme_add": "Add custom theme", - "custom_theme_downloading": "Downloading custom theme…", - "custom_theme_error_downloading": "Error downloading theme", - "custom_theme_help": "Enter the URL of a custom theme you want to apply.", - "custom_theme_invalid": "Invalid theme schema.", - "dialog_title": "Settings: Appearance", - "font_size": "Font size", - "font_size_default": "%(fontSize)s (default)", - "high_contrast": "High contrast", - "image_size_default": "Default", - "image_size_large": "Large", - "layout_bubbles": "Message bubbles", - "layout_irc": "IRC (experimental)", - "match_system_theme": "Match system theme", - "timeline_image_size": "Image size in the timeline" - }, - "automatic_language_detection_syntax_highlight": "Enable automatic language detection for syntax highlighting", - "autoplay_gifs": "Autoplay GIFs", - "autoplay_videos": "Autoplay videos", - "big_emoji": "Enable big emoji in chat", - "code_block_expand_default": "Expand code blocks by default", - "code_block_line_numbers": "Show line numbers in code blocks", - "disable_historical_profile": "Show current profile picture and name for users in message history", - "discovery": { - "title": "How to find you" - }, - "emoji_autocomplete": "Enable Emoji suggestions while typing", - "enable_markdown": "Enable Markdown", - "enable_markdown_description": "Start messages with /plain to send without markdown.", - "encryption": { - "advanced": { - "breadcrumb_first_description": "Your account details, contacts, preferences, and chat list will be kept", - "breadcrumb_page": "Reset encryption", - "breadcrumb_second_description": "You will lose any message history that’s stored only on the server", - "breadcrumb_third_description": "You will need to verify all your existing devices and contacts again", - "breadcrumb_title": "Are you sure you want to reset your identity?", - "breadcrumb_title_forgot": "Forgot your recovery key? You’ll need to reset your identity.", - "breadcrumb_warning": "Only do this if you believe your account has been compromised.", - "details_title": "Encryption details", - "export_keys": "Export keys", - "import_keys": "Import keys", - "other_people_device_description": "By default in encrypted rooms, do not send encrypted messages to anyone until you’ve verified them", - "other_people_device_label": "Never send encrypted messages to unverified devices", - "other_people_device_title": "Other people’s devices", - "reset_identity": "Reset cryptographic identity", - "session_id": "Session ID:", - "session_key": "Session key:", - "title": "Advanced" - }, - "device_not_verified_button": "Verify this device", - "device_not_verified_description": "You need to verify this device in order to view your encryption settings.", - "device_not_verified_title": "Device not verified", - "dialog_title": "Settings: Encryption", - "recovery": { - "change_recovery_confirm_button": "Confirm new recovery key", - "change_recovery_confirm_description": "Enter your new recovery key below to finish. Your old one will no longer work.", - "change_recovery_confirm_title": "Enter your new recovery key", - "change_recovery_key": "Change recovery key", - "change_recovery_key_description": "Write down this new recovery key somewhere safe. Then click Continue to confirm the change.", - "change_recovery_key_title": "Change recovery key?", - "description": "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.", - "enter_key_error": "The recovery key you entered is not correct.", - "enter_recovery_key": "Enter recovery key", - "forgot_recovery_key": "Forgot recovery key?", - "key_storage_warning": "Your key storage is out of sync. Click one of the buttons below to fix the problem.", - "save_key_description": "Do not share this with anyone!", - "save_key_title": "Recovery key", - "set_up_recovery": "Set up recovery", - "set_up_recovery_confirm_button": "Finish set up", - "set_up_recovery_confirm_description": "Enter the recovery key shown on the previous screen to finish setting up recovery.", - "set_up_recovery_confirm_title": "Enter your recovery key to confirm", - "set_up_recovery_description": "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘%(changeRecoveryKeyButton)s’.", - "set_up_recovery_save_key_description": "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.", - "set_up_recovery_save_key_title": "Save your recovery key somewhere safe", - "set_up_recovery_secondary_description": "After clicking continue, we’ll generate a recovery key for you.", - "title": "Recovery" - }, - "title": "Encryption" - }, - "general": { - "account_management_section": "Account management", - "account_section": "Account", - "add_email_dialog_title": "Add Email Address", - "add_email_failed_verification": "Failed to verify email address: make sure you clicked the link in the email", - "add_email_instructions": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", - "add_msisdn_confirm_body": "Click the button below to confirm adding this phone number.", - "add_msisdn_confirm_button": "Confirm adding phone number", - "add_msisdn_confirm_sso_button": "Confirm adding this phone number by using Single Sign On to prove your identity.", - "add_msisdn_dialog_title": "Add Phone Number", - "add_msisdn_instructions": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", - "add_msisdn_misconfigured": "The add / bind with MSISDN flow is misconfigured", - "allow_spellcheck": "Allow spell check", - "application_language": "Application language", - "application_language_reload_hint": "The app will reload after selecting another language", - "avatar_remove_progress": "Removing image...", - "avatar_save_progress": "Uploading image...", - "avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.", - "avatar_upload_error_text_generic": "The file format may not be supported.", - "avatar_upload_error_title": "Avatar image could not be uploaded", - "confirm_adding_email_body": "Click the button below to confirm adding this email address.", - "confirm_adding_email_title": "Confirm adding email", - "deactivate_confirm_body": "Are you sure you want to deactivate your account? This is irreversible.", - "deactivate_confirm_body_sso": "Confirm your account deactivation by using Single Sign On to prove your identity.", - "deactivate_confirm_content": "Confirm that you would like to deactivate your account. If you proceed:", - "deactivate_confirm_content_1": "You will not be able to reactivate your account", - "deactivate_confirm_content_2": "You will no longer be able to log in", - "deactivate_confirm_content_3": "No one will be able to reuse your username (MXID), including you: this username will remain unavailable", - "deactivate_confirm_content_4": "You will leave all rooms and DMs that you are in", - "deactivate_confirm_content_5": "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number", - "deactivate_confirm_content_6": "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?", - "deactivate_confirm_continue": "Confirm account deactivation", - "deactivate_confirm_erase_label": "Hide my messages from new joiners", - "deactivate_section": "Deactivate Account", - "deactivate_warning": "Deactivating your account is a permanent action — be careful!", - "discovery_email_empty": "Discovery options will appear once you have added an email.", - "discovery_email_verification_instructions": "Verify the link in your inbox", - "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number.", - "discovery_needs_terms": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", - "discovery_needs_terms_title": "Let people find you", - "display_name": "Display Name", - "display_name_error": "Unable to set display name", - "email_address_in_use": "This email address is already in use", - "email_address_label": "Email Address", - "email_not_verified": "Your email address hasn't been verified yet", - "email_verification_instructions": "Click the link in the email you received to verify and then click continue again.", - "emails_heading": "Email addresses", - "error_add_email": "Unable to add email address", - "error_deactivate_communication": "There was a problem communicating with the server. Please try again.", - "error_deactivate_invalid_auth": "Server did not return valid authentication information.", - "error_deactivate_no_auth": "Server did not require any authentication", - "error_email_verification": "Unable to verify email address.", - "error_invalid_email": "Invalid Email Address", - "error_invalid_email_detail": "This doesn't appear to be a valid email address", - "error_msisdn_verification": "Unable to verify phone number.", - "error_password_change_403": "Failed to change password. Is your password correct?", - "error_password_change_http": "%(errorMessage)s (HTTP status %(httpStatus)s)", - "error_password_change_title": "Error changing password", - "error_password_change_unknown": "Unknown password change error (%(stringifiedError)s)", - "error_remove_3pid": "Unable to remove contact information", - "error_revoke_email_discovery": "Unable to revoke sharing for email address", - "error_revoke_msisdn_discovery": "Unable to revoke sharing for phone number", - "error_share_email_discovery": "Unable to share email address", - "error_share_msisdn_discovery": "Unable to share phone number", - "identity_server_no_token": "No identity access token found", - "identity_server_not_set": "Identity server not set", - "language_section": "Language", - "msisdn_in_use": "This phone number is already in use", - "msisdn_label": "Phone Number", - "msisdn_verification_field_label": "Verification code", - "msisdn_verification_instructions": "Please enter verification code sent via text.", - "msisdns_heading": "Phone numbers", - "oidc_manage_button": "Manage account", - "password_change_section": "Set a new account password…", - "password_change_success": "Your password was successfully changed.", - "personal_info": "Personal info", - "profile_subtitle": "This is how you appear to others on the app.", - "profile_subtitle_oidc": "Your account is managed separately by an identity provider and so some of your personal information can’t be changed here.", - "remove_email_prompt": "Remove %(email)s?", - "remove_msisdn_prompt": "Remove %(phone)s?", - "spell_check_locale_placeholder": "Choose a locale", - "unable_to_load_emails": "Unable to load email addresses", - "unable_to_load_msisdns": "Unable to load phone numbers", - "username": "Username" - }, - "image_thumbnails": "Show previews/thumbnails for images", - "inline_url_previews_default": "Enable inline URL previews by default", - "inline_url_previews_room": "Enable URL previews by default for participants in this room", - "inline_url_previews_room_account": "Enable URL previews for this room (only affects you)", - "insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message", - "jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message", - "key_backup": { - "backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).", - "backup_starting": "Starting backup…", - "backup_success": "Success!", - "cannot_create_backup": "Unable to create key backup", - "create_title": "Create key backup", - "setup_secure_backup": { - "backup_setup_success_description": "Your keys are now being backed up from this device.", - "backup_setup_success_title": "Secure Backup successful", - "cancel_warning": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", - "confirm_security_phrase": "Confirm your Security Phrase", - "description": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", - "download_or_copy": "%(downloadButton)s or %(copyButton)s", - "enter_phrase_description": "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", - "enter_phrase_title": "Enter a Security Phrase", - "enter_phrase_to_confirm": "Enter your Security Phrase a second time to confirm it.", - "generate_security_key_description": "We'll generate a Recovery Key for you to store somewhere safe, like a password manager or a safe.", - "generate_security_key_title": "Generate a Recovery Key", - "pass_phrase_match_failed": "That doesn't match.", - "pass_phrase_match_success": "That matches!", - "phrase_strong_enough": "Great! This Security Phrase looks strong enough.", - "secret_storage_query_failure": "Unable to query secret storage status", - "security_key_safety_reminder": "Store your Recovery Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", - "set_phrase_again": "Go back to set it again.", - "settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.", - "title_confirm_phrase": "Confirm Security Phrase", - "title_save_key": "Save your Recovery Key", - "title_set_phrase": "Set a Security Phrase", - "unable_to_setup": "Unable to set up secret storage", - "use_different_passphrase": "Use a different passphrase?", - "use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Recovery Key to use for backup." - } - }, - "key_export_import": { - "confirm_passphrase": "Confirm passphrase", - "enter_passphrase": "Enter passphrase", - "export_description_1": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", - "export_description_2": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", - "export_title": "Export room keys", - "file_to_import": "File to import", - "import_description_1": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", - "import_description_2": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", - "import_title": "Import room keys", - "phrase_cannot_be_empty": "Passphrase must not be empty", - "phrase_must_match": "Passphrases must match", - "phrase_strong_enough": "Great! This passphrase looks strong enough" - }, - "keyboard": { - "dialog_title": "Settings: Keyboard", - "title": "Keyboard" - }, - "labs": { - "dialog_title": "Settings: Labs" - }, - "labs_mjolnir": { - "dialog_title": "Settings: Ignored Users" - }, - "notifications": { - "default_setting_description": "This setting will be applied by default to all your rooms.", - "default_setting_section": "I want to be notified for (Default Setting)", - "desktop_notification_message_preview": "Show message preview in desktop notification", - "dialog_title": "Settings: Notifications", - "email_description": "Receive an email summary of missed notifications", - "email_section": "Email summary", - "email_select": "Select which emails you want to send summaries to. Manage your emails in .", - "enable_audible_notifications_session": "Enable audible notifications for this session", - "enable_desktop_notifications_session": "Enable desktop notifications for this session", - "enable_email_notifications": "Enable email notifications for %(email)s", - "enable_notifications_account": "Enable notifications for this account", - "enable_notifications_account_detail": "Turn off to disable notifications on all your devices and sessions", - "enable_notifications_device": "Enable notifications for this device", - "error_loading": "There was an error loading your notification settings.", - "error_permissions_denied": "%(brand)s does not have permission to send you notifications - please check your browser settings", - "error_permissions_missing": "%(brand)s was not given permission to send notifications - please try again", - "error_saving": "Error saving notification preferences", - "error_saving_detail": "An error occurred whilst saving your notification preferences.", - "error_title": "Unable to enable Notifications", - "error_updating": "An error occurred when updating your notification preferences. Please try to toggle your option again.", - "invites": "Invited to a room", - "keywords": "Show a badge when keywords are used in a room.", - "keywords_prompt": "Enter keywords here, or use for spelling variations or nicknames", - "labs_notice_prompt": "Update:We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active. If you proceed, some of your settings may change. Learn more", - "mentions_keywords": "Mentions and Keywords", - "mentions_keywords_only": "Mentions and Keywords only", - "messages_containing_keywords": "Messages containing keywords", - "noisy": "Noisy", - "notices": "Messages sent by bots", - "notify_at_room": "Notify when someone mentions using @room", - "notify_keyword": "Notify when someone uses a keyword", - "notify_mention": "Notify when someone mentions using @displayname or %(mxid)s", - "other_section": "Other things we think you might be interested in:", - "people_mentions_keywords": "People, Mentions and Keywords", - "play_sound_for_description": "Applied by default to all rooms on all devices.", - "play_sound_for_section": "Play a sound for", - "push_targets": "Notification targets", - "quick_actions_mark_all_read": "Mark all messages as read", - "quick_actions_reset": "Reset to default settings", - "quick_actions_section": "Quick Actions", - "room_activity": "New room activity, upgrades and status messages occur", - "rule_call": "Call invitation", - "rule_contains_display_name": "Messages containing my display name", - "rule_contains_user_name": "Messages containing my username", - "rule_encrypted": "Encrypted messages in group chats", - "rule_encrypted_room_one_to_one": "Encrypted messages in one-to-one chats", - "rule_invite_for_me": "When I'm invited to a room", - "rule_message": "Messages in group chats", - "rule_room_one_to_one": "Messages in one-to-one chats", - "rule_roomnotif": "Messages containing @room", - "rule_suppress_notices": "Messages sent by bot", - "rule_tombstone": "When rooms are upgraded", - "show_message_desktop_notification": "Show message in desktop notification", - "voip": "Audio and Video calls" - }, - "preferences": { - "Electron.enableHardwareAcceleration": "Enable hardware acceleration (restart %(appName)s to take effect)", - "always_show_menu_bar": "Always show the window menu bar", - "autocomplete_delay": "Autocomplete delay (ms)", - "code_blocks_heading": "Code blocks", - "compact_modern": "Use a more compact 'Modern' layout", - "composer_heading": "Composer", - "default_timezone": "Browser default (%(timezone)s)", - "dialog_title": "Settings: Preferences", - "enable_hardware_acceleration": "Enable hardware acceleration", - "enable_tray_icon": "Show tray icon and minimise window to it on close", - "keyboard_heading": "Keyboard shortcuts", - "keyboard_view_shortcuts_button": "To view all keyboard shortcuts, click here.", - "media_heading": "Images, GIFs and videos", - "presence_description": "Share your activity and status with others.", - "publish_timezone": "Publish timezone on public profile", - "rm_lifetime": "Read Marker lifetime (ms)", - "rm_lifetime_offscreen": "Read Marker off-screen lifetime (ms)", - "room_directory_heading": "Room directory", - "room_list_heading": "Room list", - "show_avatars_pills": "Show avatars in user, room and event mentions", - "show_polls_button": "Show polls button", - "surround_text": "Surround selected text when typing special characters", - "time_heading": "Displaying time", - "user_timezone": "Set timezone" - }, - "prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs", - "replace_plain_emoji": "Automatically replace plain text Emoji", - "security": { - "4s_public_key_in_account_data": "in account data", - "4s_public_key_status": "Secret storage public key:", - "analytics_description": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", - "backup_key_cached_status": "Backup key cached:", - "backup_key_stored_status": "Backup key stored:", - "backup_key_unexpected_type": "unexpected type", - "backup_key_well_formed": "well formed", - "backup_keys_description": "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.", - "bulk_options_accept_all_invites": "Accept all %(invitedRooms)s invites", - "bulk_options_reject_all_invites": "Reject all %(invitedRooms)s invites", - "bulk_options_section": "Bulk options", - "cross_signing_cached": "cached locally", - "cross_signing_homeserver_support": "Homeserver feature support:", - "cross_signing_homeserver_support_exists": "exists", - "cross_signing_in_4s": "in secret storage", - "cross_signing_in_memory": "in memory", - "cross_signing_master_private_Key": "Master private key:", - "cross_signing_not_cached": "not found locally", - "cross_signing_not_found": "not found", - "cross_signing_not_in_4s": "not found in storage", - "cross_signing_not_stored": "not stored", - "cross_signing_private_keys": "Cross-signing private keys:", - "cross_signing_public_keys": "Cross-signing public keys:", - "cross_signing_self_signing_private_key": "Self signing private key:", - "cross_signing_user_signing_private_key": "User signing private key:", - "cryptography_section": "Cryptography", - "dehydrated_device_description": "The offline device feature allows you to receive encrypted messages even when you are not logged in to any devices", - "dehydrated_device_enabled": "Offline device enabled", - "delete_backup": "Delete Backup", - "delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", - "dialog_title": "Settings: Security & Privacy", - "e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", - "enable_message_search": "Enable message search in encrypted rooms", - "encryption_section": "Encryption", - "error_loading_key_backup_status": "Unable to load key backup status", - "export_megolm_keys": "Export E2E room keys", - "ignore_users_empty": "You have no ignored users.", - "ignore_users_section": "Ignored users", - "import_megolm_keys": "Import E2E room keys", - "key_backup_active": "This session is backing up your keys.", - "key_backup_active_version": "Active backup version:", - "key_backup_active_version_none": "None", - "key_backup_algorithm": "Algorithm:", - "key_backup_can_be_restored": "This backup can be restored on this session", - "key_backup_complete": "All keys backed up", - "key_backup_connect": "Connect this session to Key Backup", - "key_backup_connect_prompt": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.", - "key_backup_in_progress": "Backing up %(sessionsRemaining)s keys…", - "key_backup_inactive": "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", - "key_backup_inactive_warning": "Your keys are not being backed up from this session.", - "key_backup_latest_version": "Latest backup version on server:", - "message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.", - "message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.", - "message_search_enabled": { - "one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", - "other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms." - }, - "message_search_failed": "Message search initialisation failed", - "message_search_indexed_messages": "Indexed messages:", - "message_search_indexed_rooms": "Indexed rooms:", - "message_search_indexing": "Currently indexing: %(currentRoom)s", - "message_search_indexing_idle": "Not currently indexing messages for any room.", - "message_search_intro": "%(brand)s is securely caching encrypted messages locally for them to appear in search results:", - "message_search_room_progress": "%(doneRooms)s out of %(totalRooms)s", - "message_search_section": "Message search", - "message_search_sleep_time": "How fast should messages be downloaded.", - "message_search_space_used": "Space used:", - "message_search_unsupported": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", - "message_search_unsupported_web": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.", - "record_session_details": "Record the client name, version, and url to recognise sessions more easily in session manager", - "restore_key_backup": "Restore from Backup", - "secret_storage_not_ready": "not ready", - "secret_storage_ready": "ready", - "secret_storage_status": "Secret storage:", - "send_analytics": "Send analytics data", - "session_id": "Session ID:", - "session_key": "Session key:", - "strict_encryption": "Never send encrypted messages to unverified sessions from this session" - }, - "send_read_receipts": "Send read receipts", - "send_read_receipts_unsupported": "Your server doesn't support disabling sending read receipts.", - "send_typing_notifications": "Send typing notifications", - "sessions": { - "best_security_note": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", - "browser": "Browser", - "confirm_sign_out": { - "one": "Confirm signing out this device", - "other": "Confirm signing out these devices" - }, - "confirm_sign_out_body": { - "one": "Click the button below to confirm signing out this device.", - "other": "Click the button below to confirm signing out these devices." - }, - "confirm_sign_out_continue": { - "one": "Sign out device", - "other": "Sign out devices" - }, - "confirm_sign_out_sso": { - "one": "Confirm logging out this device by using Single Sign On to prove your identity.", - "other": "Confirm logging out these devices by using Single Sign On to prove your identity." - }, - "current_session": "Current session", - "desktop_session": "Desktop session", - "details_heading": "Session details", - "device_unverified_description": "Verify or sign out from this session for best security and reliability.", - "device_unverified_description_current": "Verify your current session for enhanced secure messaging.", - "device_verified_description": "This session is ready for secure messaging.", - "device_verified_description_current": "Your current session is ready for secure messaging.", - "dialog_title": "Settings: Sessions", - "error_pusher_state": "Failed to set pusher state", - "error_set_name": "Failed to set session name", - "filter_all": "All", - "filter_inactive": "Inactive", - "filter_inactive_description": "Inactive for %(inactiveAgeDays)s days or longer", - "filter_label": "Filter devices", - "filter_unverified_description": "Not ready for secure messaging", - "filter_verified_description": "Ready for secure messaging", - "hide_details": "Hide details", - "inactive_days": "Inactive for %(inactiveAgeDays)s+ days", - "inactive_sessions": "Inactive sessions", - "inactive_sessions_explainer_1": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.", - "inactive_sessions_explainer_2": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.", - "inactive_sessions_list_description": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", - "ip": "IP address", - "last_activity": "Last activity", - "manage": "Manage this session", - "mobile_session": "Mobile session", - "n_sessions_selected": { - "one": "%(count)s session selected", - "other": "%(count)s sessions selected" - }, - "no_inactive_sessions": "No inactive sessions found.", - "no_sessions": "No sessions found.", - "no_unverified_sessions": "No unverified sessions found.", - "no_verified_sessions": "No verified sessions found.", - "os": "Operating system", - "other_sessions_heading": "Other sessions", - "push_heading": "Push notifications", - "push_subheading": "Receive push notifications on this session.", - "push_toggle": "Toggle push notifications on this session.", - "rename_form_caption": "Please be aware that session names are also visible to people you communicate with.", - "rename_form_heading": "Rename session", - "rename_form_learn_more": "Renaming sessions", - "rename_form_learn_more_description_1": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.", - "rename_form_learn_more_description_2": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.", - "security_recommendations": "Security recommendations", - "security_recommendations_description": "Improve your account security by following these recommendations.", - "session_id": "Session ID", - "show_details": "Show details", - "sign_in_with_qr": "Link new device", - "sign_in_with_qr_button": "Show QR code", - "sign_in_with_qr_description": "Use a QR code to sign in to another device and set up secure messaging.", - "sign_in_with_qr_unsupported": "Not supported by your account provider", - "sign_out": "Sign out of this session", - "sign_out_all_other_sessions": "Sign out of all other sessions (%(otherSessionsCount)s)", - "sign_out_confirm_description": { - "one": "Are you sure you want to sign out of %(count)s session?", - "other": "Are you sure you want to sign out of %(count)s sessions?" - }, - "sign_out_n_sessions": { - "one": "Sign out of %(count)s session", - "other": "Sign out of %(count)s sessions" - }, - "title": "Sessions", - "unknown_session": "Unknown session type", - "unverified_session": "Unverified session", - "unverified_session_explainer_1": "This session doesn't support encryption and thus can't be verified.", - "unverified_session_explainer_2": "You won't be able to participate in rooms where encryption is enabled when using this session.", - "unverified_session_explainer_3": "For best security and privacy, it is recommended to use Matrix clients that support encryption.", - "unverified_sessions": "Unverified sessions", - "unverified_sessions_explainer_1": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", - "unverified_sessions_explainer_2": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", - "unverified_sessions_list_description": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", - "url": "URL", - "verified_session": "Verified session", - "verified_sessions": "Verified sessions", - "verified_sessions_explainer_1": "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.", - "verified_sessions_explainer_2": "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.", - "verified_sessions_list_description": "For best security, sign out from any session that you don't recognize or use anymore.", - "verify_session": "Verify session", - "web_session": "Web session" - }, - "show_avatar_changes": "Show profile picture changes", - "show_breadcrumbs": "Show shortcuts to recently viewed rooms above the room list", - "show_chat_effects": "Show chat effects (animations when receiving e.g. confetti)", - "show_displayname_changes": "Show display name changes", - "show_join_leave": "Show join/leave messages (invites/removes/bans unaffected)", - "show_nsfw_content": "Show NSFW content", - "show_read_receipts": "Show read receipts sent by other users", - "show_redaction_placeholder": "Show a placeholder for removed messages", - "show_stickers_button": "Show stickers button", - "show_typing_notifications": "Show typing notifications", - "showbold": "Show all activity in the room list (dots or number of unread messages)", - "sidebar": { - "dialog_title": "Settings: Sidebar", - "metaspaces_favourites_description": "Group all your favourite rooms and people in one place.", - "metaspaces_home_all_rooms": "Show all rooms", - "metaspaces_home_all_rooms_description": "Show all your rooms in Home, even if they're in a space.", - "metaspaces_home_description": "Home is useful for getting an overview of everything.", - "metaspaces_orphans": "Rooms outside of a space", - "metaspaces_orphans_description": "Group all your rooms that aren't part of a space in one place.", - "metaspaces_people_description": "Group all your people in one place.", - "metaspaces_subsection": "Spaces to show", - "metaspaces_video_rooms": "Video rooms and conferences", - "metaspaces_video_rooms_description": "Group all private video rooms and conferences.", - "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", - "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", - "title": "Sidebar" - }, - "start_automatically": "Start automatically after system login", - "tac_only_notifications": "Only show notifications in the thread activity centre", - "use_12_hour_format": "Show timestamps in 12 hour format (e.g. 2:30pm)", - "use_command_enter_send_message": "Use Command + Enter to send a message", - "use_command_f_search": "Use Command + F to search timeline", - "use_control_enter_send_message": "Use Ctrl + Enter to send a message", - "use_control_f_search": "Use Ctrl + F to search timeline", - "voip": { - "allow_p2p": "Allow Peer-to-Peer for 1:1 calls", - "allow_p2p_description": "When enabled, the other party might be able to see your IP address", - "audio_input_empty": "No Microphones detected", - "audio_output": "Audio Output", - "audio_output_empty": "No Audio Outputs detected", - "auto_gain_control": "Automatic gain control", - "connection_section": "Connection", - "dialog_title": "Settings: Voice & Video", - "echo_cancellation": "Echo cancellation", - "enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)", - "enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", - "mirror_local_feed": "Mirror local video feed", - "missing_permissions_prompt": "Missing media permissions, click the button below to request.", - "noise_suppression": "Noise suppression", - "request_permissions": "Request media permissions", - "title": "Voice & Video", - "video_input_empty": "No Webcams detected", - "video_section": "Video settings", - "voice_agc": "Automatically adjust the microphone volume", - "voice_processing": "Voice processing", - "voice_section": "Voice settings" - }, - "warn_quit": "Warn before quitting", - "warning": "WARNING: " - }, - "share": { - "link_copied": "Link copied", - "permalink_message": "Link to selected message", - "permalink_most_recent": "Link to most recent message", - "share_call": "Conference invite link", - "share_call_subtitle": "Link for external users to join the call without a matrix account:", - "title_link": "Share Link", - "title_message": "Share Room Message", - "title_room": "Share Room", - "title_user": "Share User" - }, - "slash_command": { - "addwidget": "Adds a custom widget by URL to the room", - "addwidget_iframe_missing_src": "iframe has no src attribute", - "addwidget_invalid_protocol": "Please supply a https:// or http:// widget URL", - "addwidget_missing_url": "Please supply a widget URL or embed code", - "addwidget_no_permissions": "You cannot modify widgets in this room.", - "ban": "Bans user with given id", - "category_actions": "Actions", - "category_admin": "Admin", - "category_advanced": "Advanced", - "category_effects": "Effects", - "category_messages": "Messages", - "category_other": "Other", - "command_error": "Command error", - "converttodm": "Converts the room to a DM", - "converttoroom": "Converts the DM to a room", - "could_not_find_room": "Could not find room", - "deop": "Deops user with given id", - "devtools": "Opens the Developer Tools dialog", - "discardsession": "Forces the current outbound group session in an encrypted room to be discarded", - "error_invalid_rendering_type": "Command error: Unable to find rendering type (%(renderingType)s)", - "error_invalid_room": "Command failed: Unable to find room (%(roomId)s)", - "error_invalid_runfn": "Command error: Unable to handle slash command.", - "error_invalid_user_in_room": "Could not find user in room", - "help": "Displays list of commands with usages and descriptions", - "help_dialog_title": "Command Help", - "holdcall": "Places the call in the current room on hold", - "html": "Sends a message as html, without interpreting it as markdown", - "ignore": "Ignores a user, hiding their messages from you", - "ignore_dialog_description": "You are now ignoring %(userId)s", - "ignore_dialog_title": "Ignored user", - "invite": "Invites user with given id to current room", - "invite_3pid_needs_is_error": "Use an identity server to invite by email. Manage in Settings.", - "invite_3pid_use_default_is_title": "Use an identity server", - "invite_3pid_use_default_is_title_description": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", - "invite_failed": "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility", - "join": "Joins room with given address", - "jumptodate": "Jump to the given date in the timeline", - "jumptodate_invalid_input": "We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.", - "lenny": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message", - "me": "Displays action", - "msg": "Sends a message to the given user", - "myavatar": "Changes your profile picture in all rooms", - "myroomavatar": "Changes your profile picture in this current room only", - "myroomnick": "Changes your display nickname in the current room only", - "nick": "Changes your display nickname", - "no_active_call": "No active call in this room", - "op": "Define the power level of a user", - "part_unknown_alias": "Unrecognised room address: %(roomAlias)s", - "plain": "Sends a message as plain text, without interpreting it as markdown", - "query": "Opens chat with the given user", - "query_not_found_phone_number": "Unable to find Matrix ID for phone number", - "rageshake": "Send a bug report with logs", - "rainbow": "Sends the given message coloured as a rainbow", - "rainbowme": "Sends the given emote coloured as a rainbow", - "remove": "Removes user with given id from this room", - "roomavatar": "Changes the avatar of the current room", - "roomname": "Sets the room name", - "server_error": "Server error", - "server_error_detail": "Server unavailable, overloaded, or something else went wrong.", - "shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", - "spoiler": "Sends the given message as a spoiler", - "tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message", - "topic": "Gets or sets the room topic", - "topic_none": "This room has no topic.", - "topic_room_error": "Failed to get room topic: Unable to find room (%(roomId)s", - "tovirtual": "Switches to this room's virtual room, if it has one", - "tovirtual_not_found": "No virtual room for this room", - "unban": "Unbans user with given ID", - "unflip": "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message", - "unholdcall": "Takes the call in the current room off hold", - "unignore": "Stops ignoring a user, showing their messages going forward", - "unignore_dialog_description": "You are no longer ignoring %(userId)s", - "unignore_dialog_title": "Unignored user", - "unknown_command": "Unknown Command", - "unknown_command_button": "Send as message", - "unknown_command_detail": "Unrecognised command: %(commandText)s", - "unknown_command_help": "You can use /help to list available commands. Did you mean to send this as a message?", - "unknown_command_hint": "Hint: Begin your message with // to start it with a slash.", - "upgraderoom": "Upgrades a room to a new version", - "upgraderoom_permission_error": "You do not have the required permissions to use this command.", - "usage": "Usage", - "view": "Views room with given address", - "whois": "Displays information about a user" - }, - "space": { - "add_existing_room_space": { - "create": "Want to add a new room instead?", - "create_prompt": "Create a new room", - "dm_heading": "Direct Messages", - "error_heading": "Not all selected were added", - "progress_text": { - "one": "Adding room...", - "other": "Adding rooms... (%(progress)s out of %(count)s)" - }, - "space_dropdown_label": "Space selection", - "space_dropdown_title": "Add existing rooms", - "subspace_moved_note": "Adding spaces has moved." - }, - "add_existing_subspace": { - "create_button": "Create a new space", - "create_prompt": "Want to add a new space instead?", - "filter_placeholder": "Search for spaces", - "space_dropdown_title": "Add existing space" - }, - "context_menu": { - "devtools_open_timeline": "See room timeline (devtools)", - "explore": "Explore rooms", - "home": "Space home", - "manage_and_explore": "Manage & explore rooms", - "options": "Space options" - }, - "failed_load_rooms": "Failed to load list of rooms.", - "failed_remove_rooms": "Failed to remove some rooms. Try again later", - "incompatible_server_hierarchy": "Your server does not support showing space hierarchies.", - "invite": "Invite people", - "invite_description": "Invite with email or username", - "invite_link": "Share invite link", - "joining_space": "Joining", - "landing_welcome": "Welcome to ", - "leave_dialog_action": "Leave space", - "leave_dialog_description": "You are about to leave .", - "leave_dialog_only_admin_room_warning": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.", - "leave_dialog_only_admin_warning": "You're the only admin of this space. Leaving it will mean no one has control over it.", - "leave_dialog_option_all": "Leave all rooms", - "leave_dialog_option_intro": "Would you like to leave the rooms in this space?", - "leave_dialog_option_none": "Don't leave any rooms", - "leave_dialog_option_specific": "Leave some rooms", - "leave_dialog_public_rejoin_warning": "You won't be able to rejoin unless you are re-invited.", - "leave_dialog_title": "Leave %(spaceName)s", - "mark_suggested": "Mark as suggested", - "no_search_result_hint": "You may want to try a different search or check for typos.", - "preferences": { - "sections_section": "Sections to show", - "show_people_in_space": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s." - }, - "room_filter_placeholder": "Search for rooms", - "search_children": "Search %(spaceName)s", - "search_placeholder": "Search names and descriptions", - "select_room_below": "Select a room below first", - "share_public": "Share your public space", - "suggested": "Suggested", - "suggested_tooltip": "This room is suggested as a good one to join", - "title_when_query_available": "Results", - "title_when_query_unavailable": "Rooms and spaces", - "unmark_suggested": "Mark as not suggested", - "user_lacks_permission": "You don't have permission" - }, - "space_settings": { - "title": "Settings - %(spaceName)s" + "prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs", + "replace_plain_emoji": "Automatically replace plain text Emoji", + "security": { + "4s_public_key_in_account_data": "in account data", + "4s_public_key_status": "Secret storage public key:", + "analytics_description": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", + "backup_key_cached_status": "Backup key cached:", + "backup_key_stored_status": "Backup key stored:", + "backup_key_unexpected_type": "unexpected type", + "backup_key_well_formed": "well formed", + "backup_keys_description": "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.", + "bulk_options_accept_all_invites": "Accept all %(invitedRooms)s invites", + "bulk_options_reject_all_invites": "Reject all %(invitedRooms)s invites", + "bulk_options_section": "Bulk options", + "cross_signing_cached": "cached locally", + "cross_signing_homeserver_support": "Homeserver feature support:", + "cross_signing_homeserver_support_exists": "exists", + "cross_signing_in_4s": "in secret storage", + "cross_signing_in_memory": "in memory", + "cross_signing_master_private_Key": "Master private key:", + "cross_signing_not_cached": "not found locally", + "cross_signing_not_found": "not found", + "cross_signing_not_in_4s": "not found in storage", + "cross_signing_not_stored": "not stored", + "cross_signing_private_keys": "Cross-signing private keys:", + "cross_signing_public_keys": "Cross-signing public keys:", + "cross_signing_self_signing_private_key": "Self signing private key:", + "cross_signing_user_signing_private_key": "User signing private key:", + "cryptography_section": "Cryptography", + "dehydrated_device_description": "The offline device feature allows you to receive encrypted messages even when you are not logged in to any devices", + "dehydrated_device_enabled": "Offline device enabled", + "delete_backup": "Delete Backup", + "delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", + "dialog_title": "Settings: Security & Privacy", + "e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", + "enable_message_search": "Enable message search in encrypted rooms", + "encryption_section": "Encryption", + "error_loading_key_backup_status": "Unable to load key backup status", + "export_megolm_keys": "Export E2E room keys", + "ignore_users_empty": "You have no ignored users.", + "ignore_users_section": "Ignored users", + "import_megolm_keys": "Import E2E room keys", + "key_backup_active": "This session is backing up your keys.", + "key_backup_active_version": "Active backup version:", + "key_backup_active_version_none": "None", + "key_backup_algorithm": "Algorithm:", + "key_backup_can_be_restored": "This backup can be restored on this session", + "key_backup_complete": "All keys backed up", + "key_backup_connect": "Connect this session to Key Backup", + "key_backup_connect_prompt": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.", + "key_backup_in_progress": "Backing up %(sessionsRemaining)s keys…", + "key_backup_inactive": "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", + "key_backup_inactive_warning": "Your keys are not being backed up from this session.", + "key_backup_latest_version": "Latest backup version on server:", + "message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.", + "message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.", + "message_search_enabled": { + "one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", + "other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms." + }, + "message_search_failed": "Message search initialisation failed", + "message_search_indexed_messages": "Indexed messages:", + "message_search_indexed_rooms": "Indexed rooms:", + "message_search_indexing": "Currently indexing: %(currentRoom)s", + "message_search_indexing_idle": "Not currently indexing messages for any room.", + "message_search_intro": "%(brand)s is securely caching encrypted messages locally for them to appear in search results:", + "message_search_room_progress": "%(doneRooms)s out of %(totalRooms)s", + "message_search_section": "Message search", + "message_search_sleep_time": "How fast should messages be downloaded.", + "message_search_space_used": "Space used:", + "message_search_unsupported": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", + "message_search_unsupported_web": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.", + "record_session_details": "Record the client name, version, and url to recognise sessions more easily in session manager", + "restore_key_backup": "Restore from Backup", + "secret_storage_not_ready": "not ready", + "secret_storage_ready": "ready", + "secret_storage_status": "Secret storage:", + "send_analytics": "Send analytics data", + "session_id": "Session ID:", + "session_key": "Session key:", + "strict_encryption": "Never send encrypted messages to unverified sessions from this session" }, - "spaces": { - "error_no_permission_add_room": "You do not have permissions to add rooms to this space", - "error_no_permission_add_space": "You do not have permissions to add spaces to this space", - "error_no_permission_create_room": "You do not have permissions to create new rooms in this space", - "error_no_permission_invite": "You do not have permissions to invite people to this space" - }, - "spotlight": { - "public_rooms": { - "network_dropdown_add_dialog_description": "Enter the name of a new server you want to explore.", - "network_dropdown_add_dialog_placeholder": "Server name", - "network_dropdown_add_dialog_title": "Add a new server", - "network_dropdown_add_server_option": "Add new server…", - "network_dropdown_available_invalid": "Can't find this server or its room list", - "network_dropdown_available_invalid_forbidden": "You are not allowed to view this server's rooms list", - "network_dropdown_available_valid": "Looks good", - "network_dropdown_remove_server_adornment": "Remove server “%(roomServer)s”", - "network_dropdown_required_invalid": "Enter a server name", - "network_dropdown_selected_label": "Show: Matrix rooms", - "network_dropdown_selected_label_instance": "Show: %(instance)s rooms (%(server)s)", - "network_dropdown_your_server_description": "Your server" - } - }, - "spotlight_dialog": { - "cant_find_person_helpful_hint": "If you can't see who you're looking for, send them your invite link.", - "cant_find_room_helpful_hint": "If you can't find the room you're looking for, ask for an invite or create a new room.", - "copy_link_text": "Copy invite link", - "count_of_members": { - "one": "%(count)s Member", - "other": "%(count)s Members" - }, - "create_new_room_button": "Create new room", - "failed_querying_public_rooms": "Failed to query public rooms", - "failed_querying_public_spaces": "Failed to query public spaces", - "group_chat_section_title": "Other options", - "heading_with_query": "Use \"%(query)s\" to search", - "heading_without_query": "Search for", - "join_button_text": "Join %(roomAddress)s", - "keyboard_scroll_hint": "Use to scroll", - "message_search_section_title": "Other searches", - "other_rooms_in_space": "Other rooms in %(spaceName)s", - "public_rooms_label": "Public rooms", - "public_spaces_label": "Public spaces", - "recent_searches_section_title": "Recent searches", - "recently_viewed_section_title": "Recently viewed", - "remove_filter": "Remove search filter for %(filter)s", - "result_may_be_hidden_privacy_warning": "Some results may be hidden for privacy", - "result_may_be_hidden_warning": "Some results may be hidden", - "search_dialog": "Search Dialog", - "search_messages_hint": "To search messages, look for this icon at the top of a room ", - "spaces_title": "Spaces you're in", - "start_group_chat_button": "Start a group chat" - }, - "stickers": { - "empty": "You don't currently have any stickerpacks enabled", - "empty_add_prompt": "Add some now" - }, - "terms": { - "column_document": "Document", - "column_service": "Service", - "column_summary": "Summary", - "identity_server_no_terms_description_1": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", - "identity_server_no_terms_description_2": "Only continue if you trust the owner of the server.", - "identity_server_no_terms_title": "Identity server has no terms of service", - "inline_intro_text": "Accept to continue:", - "integration_manager": "Use bots, bridges, widgets and sticker packs", - "intro": "To continue you need to accept the terms of this service.", - "summary_identity_server_1": "Find others by phone or email", - "summary_identity_server_2": "Be found by phone or email", - "tac_button": "Review terms and conditions", - "tac_description": "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.", - "tac_title": "Terms and Conditions", - "tos": "Terms of Service" - }, - "theme": { - "light_high_contrast": "Light high contrast", - "match_system": "Match system" - }, - "thread_view_back_action_label": "Back to thread", - "threads": { - "all_threads": "All threads", - "all_threads_description": "Shows all threads from current room", - "count_of_reply": { - "one": "%(count)s reply", - "other": "%(count)s replies" - }, - "empty_description": "Use “%(replyInThread)s” when hovering over a message.", - "empty_title": "Threads help keep your conversations on-topic and easy to track.", - "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", - "mark_all_read": "Mark all as read", - "my_threads": "My threads", - "my_threads_description": "Shows all threads you've participated in", - "open_thread": "Open thread", - "show_thread_filter": "Show:" - }, - "threads_activity_centre": { - "header": "Threads activity", - "no_rooms_with_threads_notifs": "You don't have rooms with thread notifications yet.", - "no_rooms_with_unread_threads": "You don't have rooms with unread threads yet.", - "release_announcement_description": "Threads notifications have moved, find them here from now on.", - "release_announcement_header": "Threads Activity Centre" - }, - "time": { - "about_day_ago": "about a day ago", - "about_hour_ago": "about an hour ago", - "about_minute_ago": "about a minute ago", - "date_at_time": "%(date)s at %(time)s", - "few_seconds_ago": "a few seconds ago", - "hours_minutes_seconds_left": "%(hours)sh %(minutes)sm %(seconds)ss left", - "in_about_day": "about a day from now", - "in_about_hour": "about an hour from now", - "in_about_minute": "about a minute from now", - "in_few_seconds": "a few seconds from now", - "in_n_days": "%(num)s days from now", - "in_n_hours": "%(num)s hours from now", - "in_n_minutes": "%(num)s minutes from now", - "left": "%(timeRemaining)s left", - "minutes_seconds_left": "%(minutes)sm %(seconds)ss left", - "n_days_ago": "%(num)s days ago", - "n_hours_ago": "%(num)s hours ago", - "n_minutes_ago": "%(num)s minutes ago", - "seconds_left": "%(seconds)ss left", - "short_days": "%(value)sd", - "short_days_hours_minutes_seconds": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", - "short_hours": "%(value)sh", - "short_hours_minutes_seconds": "%(hours)sh %(minutes)sm %(seconds)ss", - "short_minutes": "%(value)sm", - "short_minutes_seconds": "%(minutes)sm %(seconds)ss", - "short_seconds": "%(value)ss" - }, - "timeline": { - "context_menu": { - "collapse_reply_thread": "Collapse reply thread", - "external_url": "Source URL", - "open_in_osm": "Open in OpenStreetMap", - "report": "Report", - "resent_unsent_reactions": "Resend %(unsentCount)s reaction(s)", - "show_url_preview": "Show preview", - "view_related_event": "View related event", - "view_source": "View source" - }, - "creation_summary_dm": "%(creator)s created this DM.", - "creation_summary_room": "%(creator)s created and configured the room.", - "decryption_failure": { - "blocked": "The sender has blocked you from receiving this message because your device is unverified", - "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_user_not_joined": "You don't have access to this message", - "sender_identity_previously_verified": "Sender's verified identity has changed", - "sender_unsigned_device": "Sent from an insecure device.", - "unable_to_decrypt": "Unable to decrypt message" - }, - "disambiguated_profile": "%(displayName)s (%(matrixId)s)", - "download_action_decrypting": "Decrypting", - "download_action_downloading": "Downloading", - "download_failed": "Download failed", - "download_failed_description": "An error occurred while downloading this file", - "e2e_state": "State of the end-to-end encryption", - "edits": { - "tooltip_label": "Edited at %(date)s. Click to view edits.", - "tooltip_sub": "Click to view edits", - "tooltip_title": "Edited at %(date)s" - }, - "error_no_renderer": "This event could not be displayed", - "error_rendering_message": "Can't load this message", - "historical_messages_unavailable": "You can't see earlier messages", - "in_room_name": " in %(room)s", - "io.element.widgets.layout": "%(senderName)s has updated the room layout", - "late_event_separator": "Originally sent %(dateTime)s", - "load_error": { - "no_permission": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", - "title": "Failed to load timeline position", - "unable_to_find": "Tried to load a specific point in this room's timeline, but was unable to find it." - }, - "m.audio": { - "error_downloading_audio": "Error downloading audio", - "error_processing_audio": "Error processing audio message", - "error_processing_voice_message": "Error processing voice message", - "unnamed_audio": "Unnamed audio" - }, - "m.beacon_info": { - "view_live_location": "View live location" - }, - "m.call": { - "video_call_ended": "Video call ended", - "video_call_started": "Video call started in %(roomName)s.", - "video_call_started_text": "%(name)s started a video call", - "video_call_started_unsupported": "Video call started in %(roomName)s. (not supported by this browser)" - }, - "m.call.hangup": { - "dm": "Call ended" - }, - "m.call.invite": { - "answered_elsewhere": "Answered elsewhere", - "call_back_prompt": "Call back", - "declined": "Call declined", - "failed_connect_media": "Could not connect media", - "failed_connection": "Connection failed", - "failed_opponent_media": "Their device couldn't start the camera or microphone", - "missed_call": "Missed call", - "no_answer": "No answer", - "unknown_error": "An unknown error occurred", - "unknown_failure": "Unknown failure: %(reason)s", - "unknown_state": "The call is in an unknown state!", - "video_call": "%(senderName)s placed a video call.", - "video_call_unsupported": "%(senderName)s placed a video call. (not supported by this browser)", - "voice_call": "%(senderName)s placed a voice call.", - "voice_call_unsupported": "%(senderName)s placed a voice call. (not supported by this browser)" - }, - "m.file": { - "error_decrypting": "Error decrypting attachment", - "error_invalid": "Invalid file" - }, - "m.image": { - "error": "Unable to show image due to error", - "error_decrypting": "Error decrypting image", - "error_downloading": "Error downloading image", - "sent": "%(senderDisplayName)s sent an image.", - "show_image": "Show image" - }, - "m.key.verification.request": { - "user_wants_to_verify": "%(name)s wants to verify", - "you_started": "You sent a verification request" - }, - "m.location": { - "full": "%(senderName)s has shared their location", - "location": "Shared a location: ", - "self_location": "Shared their location: " - }, - "m.poll": { - "count_of_votes": { - "one": "%(count)s vote", - "other": "%(count)s votes" - } - }, - "m.poll.end": { - "ended": "Ended a poll", - "sender_ended": "%(senderName)s has ended a poll" - }, - "m.poll.start": "%(senderName)s has started a poll - %(pollQuestion)s", - "m.room.avatar": { - "changed": "%(senderDisplayName)s changed the room avatar.", - "changed_img": "%(senderDisplayName)s changed the room avatar to ", - "lightbox_title": "%(senderDisplayName)s changed the avatar for %(roomName)s", - "removed": "%(senderDisplayName)s removed the room avatar." - }, - "m.room.canonical_alias": { - "alt_added": { - "one": "%(senderName)s added alternative address %(addresses)s for this room.", - "other": "%(senderName)s added the alternative addresses %(addresses)s for this room." - }, - "alt_removed": { - "one": "%(senderName)s removed alternative address %(addresses)s for this room.", - "other": "%(senderName)s removed the alternative addresses %(addresses)s for this room." - }, - "changed": "%(senderName)s changed the addresses for this room.", - "changed_alternative": "%(senderName)s changed the alternative addresses for this room.", - "changed_main_and_alternative": "%(senderName)s changed the main and alternative addresses for this room.", - "removed": "%(senderName)s removed the main address for this room.", - "set": "%(senderName)s set the main address for this room to %(address)s." - }, - "m.room.create": { - "continuation": "This room is a continuation of another conversation.", - "see_older_messages": "Click here to see older messages.", - "unknown_predecessor": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.", - "unknown_predecessor_guess_server": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:" - }, - "m.room.encryption": { - "disable_attempt": "Ignored attempt to disable encryption", - "disabled": "Encryption not enabled", - "enabled": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.", - "enabled_dm": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.", - "enabled_local": "Messages in this chat will be end-to-end encrypted.", - "parameters_changed": "Some encryption parameters have been changed.", - "unsupported": "The encryption used by this room isn't supported." - }, - "m.room.guest_access": { - "can_join": "%(senderDisplayName)s has allowed guests to join the room.", - "forbidden": "%(senderDisplayName)s has prevented guests from joining the room.", - "unknown": "%(senderDisplayName)s changed guest access to %(rule)s" - }, - "m.room.history_visibility": { - "invited": "%(senderName)s made future room history visible to all room members, from the point they are invited.", - "joined": "%(senderName)s made future room history visible to all room members, from the point they joined.", - "shared": "%(senderName)s made future room history visible to all room members.", - "unknown": "%(senderName)s made future room history visible to unknown (%(visibility)s).", - "world_readable": "%(senderName)s made future room history visible to anyone." - }, - "m.room.join_rules": { - "invite": "%(senderDisplayName)s made the room invite only.", - "knock": "%(senderDisplayName)s changed the join rule to ask to join.", - "public": "%(senderDisplayName)s made the room public to whoever knows the link.", - "restricted": "%(senderDisplayName)s changed who can join this room.", - "restricted_settings": "%(senderDisplayName)s changed who can join this room. View settings.", - "unknown": "%(senderDisplayName)s changed the join rule to %(rule)s" - }, - "m.room.member": { - "accepted_3pid_invite": "%(targetName)s accepted the invitation for %(displayName)s", - "accepted_invite": "%(targetName)s accepted an invitation", - "ban": "%(senderName)s banned %(targetName)s", - "ban_reason": "%(senderName)s banned %(targetName)s: %(reason)s", - "change_avatar": "%(senderName)s changed their profile picture", - "change_name": "%(oldDisplayName)s changed their display name to %(displayName)s", - "change_name_avatar": "%(oldDisplayName)s changed their display name and profile picture", - "invite": "%(senderName)s invited %(targetName)s", - "join": "%(targetName)s joined the room", - "kick": "%(senderName)s removed %(targetName)s", - "kick_reason": "%(senderName)s removed %(targetName)s: %(reason)s", - "left": "%(targetName)s left the room", - "left_reason": "%(targetName)s left the room: %(reason)s", - "no_change": "%(senderName)s made no change", - "reject_invite": "%(targetName)s rejected the invitation", - "reject_invite_reason": "%(targetName)s rejected the invitation: %(reason)s", - "remove_avatar": "%(senderName)s removed their profile picture", - "remove_name": "%(senderName)s removed their display name (%(oldDisplayName)s)", - "set_avatar": "%(senderName)s set a profile picture", - "set_name": "%(senderName)s set their display name to %(displayName)s", - "unban": "%(senderName)s unbanned %(targetName)s", - "withdrew_invite": "%(senderName)s withdrew %(targetName)s's invitation", - "withdrew_invite_reason": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s" - }, - "m.room.name": { - "change": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.", - "remove": "%(senderDisplayName)s removed the room name.", - "set": "%(senderDisplayName)s changed the room name to %(roomName)s." - }, - "m.room.pinned_events": { - "changed": "%(senderName)s changed the pinned messages for the room.", - "changed_link": "%(senderName)s changed the pinned messages for the room.", - "pinned": "%(senderName)s pinned a message to this room. See all pinned messages.", - "pinned_link": "%(senderName)s pinned a message to this room. See all pinned messages.", - "unpinned": "%(senderName)s unpinned a message from this room. See all pinned messages.", - "unpinned_link": "%(senderName)s unpinned a message from this room. See all pinned messages." - }, - "m.room.power_levels": { - "changed": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", - "user_from_to": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s" - }, - "m.room.server_acl": { - "all_servers_banned": "🎉 All servers are banned from participating! This room can no longer be used.", - "changed": "%(senderDisplayName)s changed the server ACLs for this room.", - "set": "%(senderDisplayName)s set the server ACLs for this room." - }, - "m.room.third_party_invite": { - "revoked": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", - "sent": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room." - }, - "m.room.tombstone": "%(senderDisplayName)s upgraded this room.", - "m.room.topic": { - "changed": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", - "removed": "%(senderDisplayName)s removed the topic." - }, - "m.sticker": "%(senderDisplayName)s sent a sticker.", - "m.video": { - "error_decrypting": "Error decrypting video" - }, - "m.widget": { - "added": "%(widgetName)s widget added by %(senderName)s", - "jitsi_ended": "Video conference ended by %(senderName)s", - "jitsi_join_right_prompt": "Join the conference from the room information card on the right", - "jitsi_join_top_prompt": "Join the conference at the top of this room", - "jitsi_started": "Video conference started by %(senderName)s", - "jitsi_updated": "Video conference updated by %(senderName)s", - "modified": "%(widgetName)s widget modified by %(senderName)s", - "removed": "%(widgetName)s widget removed by %(senderName)s" - }, - "mab": { - "collapse_reply_chain": "Collapse quotes", - "copy_link_thread": "Copy link to thread", - "expand_reply_chain": "Expand quotes", - "label": "Message Actions", - "view_in_room": "View in room" - }, - "message_timestamp_received_at": "Received at: %(dateTime)s", - "message_timestamp_sent_at": "Sent at: %(dateTime)s", - "mjolnir": { - "changed_rule_glob": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", - "changed_rule_rooms": "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", - "changed_rule_servers": "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", - "changed_rule_users": "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", - "created_rule": "%(senderName)s created a ban rule matching %(glob)s for %(reason)s", - "created_rule_rooms": "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", - "created_rule_servers": "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", - "created_rule_users": "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", - "message_hidden": "You have ignored this user, so their message is hidden. Show anyways.", - "removed_rule": "%(senderName)s removed a ban rule matching %(glob)s", - "removed_rule_rooms": "%(senderName)s removed the rule banning rooms matching %(glob)s", - "removed_rule_servers": "%(senderName)s removed the rule banning servers matching %(glob)s", - "removed_rule_users": "%(senderName)s removed the rule banning users matching %(glob)s", - "updated_invalid_rule": "%(senderName)s updated an invalid ban rule", - "updated_rule": "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", - "updated_rule_rooms": "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", - "updated_rule_servers": "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", - "updated_rule_users": "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s" - }, - "no_permission_messages_before_invite": "You don't have permission to view messages from before you were invited.", - "no_permission_messages_before_join": "You don't have permission to view messages from before you joined.", - "pending_moderation": "Message pending moderation", - "pending_moderation_reason": "Message pending moderation: %(reason)s", - "reactions": { - "add_reaction_prompt": "Add reaction", - "custom_reaction_fallback_label": "Custom reaction", - "label": "%(reactors)s reacted with %(content)s", - "tooltip_caption": "reacted with %(shortName)s" - }, - "read_receipt_title": { - "one": "Seen by %(count)s person", - "other": "Seen by %(count)s people" - }, - "read_receipts_label": "Read receipts", - "redacted": { - "tooltip": "Message deleted on %(date)s" - }, - "redaction": "Message deleted by %(name)s", - "reply": { - "error_loading": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", - "in_reply_to": "In reply to ", - "in_reply_to_for_export": "In reply to this message" - }, - "scalar_starter_link": { - "dialog_description": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", - "dialog_title": "Add an Integration" - }, - "self_redaction": "Message deleted", - "send_state_encrypting": "Encrypting your message…", - "send_state_failed": "Failed to send", - "send_state_sending": "Sending your message…", - "send_state_sent": "Your message was sent", - "summary": { - "banned": { - "one": "was banned", - "other": "was banned %(count)s times" - }, - "banned_multiple": { - "one": "were banned", - "other": "were banned %(count)s times" - }, - "changed_avatar": { - "one": "%(oneUser)schanged their profile picture", - "other": "%(oneUser)schanged their profile picture %(count)s times" - }, - "changed_avatar_multiple": { - "one": "%(severalUsers)schanged their profile picture", - "other": "%(severalUsers)schanged their profile picture %(count)s times" - }, - "changed_name": { - "one": "%(oneUser)schanged their name", - "other": "%(oneUser)schanged their name %(count)s times" - }, - "changed_name_multiple": { - "one": "%(severalUsers)schanged their name", - "other": "%(severalUsers)schanged their name %(count)s times" - }, - "format": "%(nameList)s %(transitionList)s", - "hidden_event": { - "one": "%(oneUser)ssent a hidden message", - "other": "%(oneUser)ssent %(count)s hidden messages" - }, - "hidden_event_multiple": { - "one": "%(severalUsers)ssent a hidden message", - "other": "%(severalUsers)ssent %(count)s hidden messages" - }, - "invite_withdrawn": { - "one": "%(oneUser)shad their invitation withdrawn", - "other": "%(oneUser)shad their invitation withdrawn %(count)s times" - }, - "invite_withdrawn_multiple": { - "one": "%(severalUsers)shad their invitations withdrawn", - "other": "%(severalUsers)shad their invitations withdrawn %(count)s times" - }, - "invited": { - "one": "was invited", - "other": "was invited %(count)s times" - }, - "invited_multiple": { - "one": "were invited", - "other": "were invited %(count)s times" - }, - "joined": { - "one": "%(oneUser)sjoined", - "other": "%(oneUser)sjoined %(count)s times" - }, - "joined_and_left": { - "one": "%(oneUser)sjoined and left", - "other": "%(oneUser)sjoined and left %(count)s times" - }, - "joined_and_left_multiple": { - "one": "%(severalUsers)sjoined and left", - "other": "%(severalUsers)sjoined and left %(count)s times" - }, - "joined_multiple": { - "one": "%(severalUsers)sjoined", - "other": "%(severalUsers)sjoined %(count)s times" - }, - "kicked": { - "one": "was removed", - "other": "was removed %(count)s times" - }, - "kicked_multiple": { - "one": "were removed", - "other": "were removed %(count)s times" - }, - "left": { - "one": "%(oneUser)sleft", - "other": "%(oneUser)sleft %(count)s times" - }, - "left_multiple": { - "one": "%(severalUsers)sleft", - "other": "%(severalUsers)sleft %(count)s times" - }, - "no_change": { - "one": "%(oneUser)smade no changes", - "other": "%(oneUser)smade no changes %(count)s times" - }, - "no_change_multiple": { - "one": "%(severalUsers)smade no changes", - "other": "%(severalUsers)smade no changes %(count)s times" - }, - "pinned_events": { - "one": "%(oneUser)schanged the pinned messages for the room", - "other": "%(oneUser)schanged the pinned messages for the room %(count)s times" - }, - "pinned_events_multiple": { - "one": "%(severalUsers)schanged the pinned messages for the room", - "other": "%(severalUsers)schanged the pinned messages for the room %(count)s times" - }, - "redacted": { - "one": "%(oneUser)sremoved a message", - "other": "%(oneUser)sremoved %(count)s messages" - }, - "redacted_multiple": { - "one": "%(severalUsers)sremoved a message", - "other": "%(severalUsers)sremoved %(count)s messages" - }, - "rejected_invite": { - "one": "%(oneUser)srejected their invitation", - "other": "%(oneUser)srejected their invitation %(count)s times" - }, - "rejected_invite_multiple": { - "one": "%(severalUsers)srejected their invitations", - "other": "%(severalUsers)srejected their invitations %(count)s times" - }, - "rejoined": { - "one": "%(oneUser)sleft and rejoined", - "other": "%(oneUser)sleft and rejoined %(count)s times" - }, - "rejoined_multiple": { - "one": "%(severalUsers)sleft and rejoined", - "other": "%(severalUsers)sleft and rejoined %(count)s times" - }, - "server_acls": { - "one": "%(oneUser)schanged the server ACLs", - "other": "%(oneUser)schanged the server ACLs %(count)s times" - }, - "server_acls_multiple": { - "one": "%(severalUsers)schanged the server ACLs", - "other": "%(severalUsers)schanged the server ACLs %(count)s times" - }, - "unbanned": { - "one": "was unbanned", - "other": "was unbanned %(count)s times" - }, - "unbanned_multiple": { - "one": "were unbanned", - "other": "were unbanned %(count)s times" - } - }, - "thread_info_basic": "From a thread", - "typing_indicator": { - "more_users": { - "one": "%(names)s and one other is typing …", - "other": "%(names)s and %(count)s others are typing …" - }, - "one_user": "%(displayName)s is typing …", - "two_users": "%(names)s and %(lastPerson)s are typing …" - }, - "undecryptable_tooltip": "This message could not be decrypted", - "url_preview": { - "close": "Close preview", - "show_n_more": { - "one": "Show %(count)s other preview", - "other": "Show %(count)s other previews" - } - } - }, - "truncated_list_n_more": { - "other": "And %(count)s more..." - }, - "unsupported_browser": { - "description": "If you continue, some features may stop working and there is a risk that you may lose data in the future. Update your browser to continue using %(brand)s.", - "title": "%(brand)s does not support this browser" - }, - "unsupported_server_description": "This server is using an older version of Matrix. Upgrade to Matrix %(version)s to use %(brand)s without errors.", - "unsupported_server_title": "Your server is unsupported", - "update": { - "changelog": "Changelog", - "check_action": "Check for update", - "checking": "Checking for an update…", - "downloading": "Downloading update…", - "error_encountered": "Error encountered (%(errorDetail)s).", - "error_unable_load_commit": "Unable to load commit detail: %(msg)s", - "new_version_available": "New version available. Update now.", - "no_update": "No update available.", - "release_notes_toast_title": "What's New", - "see_changes_button": "What's new?", - "toast_description": "New version of %(brand)s is available", - "toast_title": "Update %(brand)s", - "unavailable": "Unavailable" - }, - "update_room_access_modal": { - "description": "To create a share link, you need to allow guests to join this room. This may make the room less secure. When you're done with the call, you can make the room private again.", - "dont_change_description": "Alternatively, you can hold the call in a separate room.", - "no_change": "I don't want to change the access level.", - "title": "Change the room access level" - }, - "upload_failed_generic": "The file '%(fileName)s' failed to upload.", - "upload_failed_size": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", - "upload_failed_title": "Upload Failed", - "upload_file": { - "cancel_all_button": "Cancel All", - "error_file_too_large": "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", - "error_files_too_large": "These files are too large to upload. The file size limit is %(limit)s.", - "error_some_files_too_large": "Some files are too large to be uploaded. The file size limit is %(limit)s.", - "error_title": "Upload Error", - "not_image": "The file you have chosen is not a valid image file.", - "title": "Upload files", - "title_progress": "Upload files (%(current)s of %(total)s)", - "upload_all_button": "Upload all", - "upload_n_others_button": { - "one": "Upload %(count)s other file", - "other": "Upload %(count)s other files" - } - }, - "user_info": { - "admin_tools_section": "Admin Tools", - "ban_button_room": "Ban from room", - "ban_button_space": "Ban from space", - "ban_room_confirm_title": "Ban from %(roomName)s", - "ban_space_everything": "Ban them from everything I'm able to", - "ban_space_specific": "Ban them from specific things I'm able to", - "deactivate_confirm_action": "Deactivate user", - "deactivate_confirm_description": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", - "deactivate_confirm_title": "Deactivate user?", - "demote_button": "Demote", - "demote_self_confirm_description_space": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.", - "demote_self_confirm_room": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", - "demote_self_confirm_title": "Demote yourself?", - "disinvite_button_room": "Disinvite from room", - "disinvite_button_room_name": "Disinvite from %(roomName)s", - "disinvite_button_space": "Disinvite from space", - "error_ban_user": "Failed to ban user", - "error_deactivate": "Failed to deactivate user", - "error_kicking_user": "Failed to remove user", - "error_mute_user": "Failed to mute user", - "error_revoke_3pid_invite_description": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.", - "error_revoke_3pid_invite_title": "Failed to revoke invite", - "ignore_button": "Ignore", - "ignore_confirm_description": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?", - "ignore_confirm_title": "Ignore %(user)s", - "invited_by": "Invited by %(sender)s", - "jump_to_rr_button": "Jump to read receipt", - "kick_button_room": "Remove from room", - "kick_button_room_name": "Remove from %(roomName)s", - "kick_button_space": "Remove from space", - "kick_button_space_everything": "Remove them from everything I'm able to", - "kick_space_specific": "Remove them from specific things I'm able to", - "kick_space_warning": "They'll still be able to access whatever you're not an admin of.", - "promote_warning": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", - "redact": { - "confirm_button": { - "one": "Remove 1 message", - "other": "Remove %(count)s messages" - }, - "confirm_description_1": { - "one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", - "other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?" - }, - "confirm_description_2": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", - "confirm_keep_state_explainer": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", - "confirm_keep_state_label": "Preserve system messages", - "confirm_title": "Remove recent messages by %(user)s", - "no_recent_messages_description": "Try scrolling up in the timeline to see if there are any earlier ones.", - "no_recent_messages_title": "No recent messages by %(user)s found" - }, - "redact_button": "Remove messages", - "revoke_invite": "Revoke invite", - "room_encrypted": "Messages in this room are end-to-end encrypted.", - "room_encrypted_detail": "Your messages are secured and only you and the recipient have the unique keys to unlock them.", - "room_unencrypted": "Messages in this room are not end-to-end encrypted.", - "room_unencrypted_detail": "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.", - "send_message": "Send message", - "share_button": "Share profile", - "unban_button_room": "Unban from room", - "unban_button_space": "Unban from space", - "unban_room_confirm_title": "Unban from %(roomName)s", - "unban_space_everything": "Unban them from everything I'm able to", - "unban_space_specific": "Unban them from specific things I'm able to", - "unban_space_warning": "They won't be able to access whatever you're not an admin of.", - "unignore_button": "Unignore", - "verification_unavailable": "User verification unavailable", - "verify_button": "Verify User", - "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." - }, - "user_menu": { - "link_new_device": "Link new device", - "settings": "All settings", - "switch_theme_dark": "Switch to dark mode", - "switch_theme_light": "Switch to light mode" + "send_read_receipts": "Send read receipts", + "send_read_receipts_unsupported": "Your server doesn't support disabling sending read receipts.", + "send_typing_notifications": "Send typing notifications", + "sessions": { + "best_security_note": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", + "browser": "Browser", + "confirm_sign_out": { + "one": "Confirm signing out this device", + "other": "Confirm signing out these devices" + }, + "confirm_sign_out_body": { + "one": "Click the button below to confirm signing out this device.", + "other": "Click the button below to confirm signing out these devices." + }, + "confirm_sign_out_continue": { + "one": "Sign out device", + "other": "Sign out devices" + }, + "confirm_sign_out_sso": { + "one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "other": "Confirm logging out these devices by using Single Sign On to prove your identity." + }, + "current_session": "Current session", + "desktop_session": "Desktop session", + "details_heading": "Session details", + "device_unverified_description": "Verify or sign out from this session for best security and reliability.", + "device_unverified_description_current": "Verify your current session for enhanced secure messaging.", + "device_verified_description": "This session is ready for secure messaging.", + "device_verified_description_current": "Your current session is ready for secure messaging.", + "dialog_title": "Settings: Sessions", + "error_pusher_state": "Failed to set pusher state", + "error_set_name": "Failed to set session name", + "filter_all": "All", + "filter_inactive": "Inactive", + "filter_inactive_description": "Inactive for %(inactiveAgeDays)s days or longer", + "filter_label": "Filter devices", + "filter_unverified_description": "Not ready for secure messaging", + "filter_verified_description": "Ready for secure messaging", + "hide_details": "Hide details", + "inactive_days": "Inactive for %(inactiveAgeDays)s+ days", + "inactive_sessions": "Inactive sessions", + "inactive_sessions_explainer_1": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.", + "inactive_sessions_explainer_2": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.", + "inactive_sessions_list_description": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", + "ip": "IP address", + "last_activity": "Last activity", + "manage": "Manage this session", + "mobile_session": "Mobile session", + "n_sessions_selected": { + "one": "%(count)s session selected", + "other": "%(count)s sessions selected" + }, + "no_inactive_sessions": "No inactive sessions found.", + "no_sessions": "No sessions found.", + "no_unverified_sessions": "No unverified sessions found.", + "no_verified_sessions": "No verified sessions found.", + "os": "Operating system", + "other_sessions_heading": "Other sessions", + "push_heading": "Push notifications", + "push_subheading": "Receive push notifications on this session.", + "push_toggle": "Toggle push notifications on this session.", + "rename_form_caption": "Please be aware that session names are also visible to people you communicate with.", + "rename_form_heading": "Rename session", + "rename_form_learn_more": "Renaming sessions", + "rename_form_learn_more_description_1": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.", + "rename_form_learn_more_description_2": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.", + "security_recommendations": "Security recommendations", + "security_recommendations_description": "Improve your account security by following these recommendations.", + "session_id": "Session ID", + "show_details": "Show details", + "sign_in_with_qr": "Link new device", + "sign_in_with_qr_button": "Show QR code", + "sign_in_with_qr_description": "Use a QR code to sign in to another device and set up secure messaging.", + "sign_in_with_qr_unsupported": "Not supported by your account provider", + "sign_out": "Sign out of this session", + "sign_out_all_other_sessions": "Sign out of all other sessions (%(otherSessionsCount)s)", + "sign_out_confirm_description": { + "one": "Are you sure you want to sign out of %(count)s session?", + "other": "Are you sure you want to sign out of %(count)s sessions?" + }, + "sign_out_n_sessions": { + "one": "Sign out of %(count)s session", + "other": "Sign out of %(count)s sessions" + }, + "title": "Sessions", + "unknown_session": "Unknown session type", + "unverified_session": "Unverified session", + "unverified_session_explainer_1": "This session doesn't support encryption and thus can't be verified.", + "unverified_session_explainer_2": "You won't be able to participate in rooms where encryption is enabled when using this session.", + "unverified_session_explainer_3": "For best security and privacy, it is recommended to use Matrix clients that support encryption.", + "unverified_sessions": "Unverified sessions", + "unverified_sessions_explainer_1": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", + "unverified_sessions_explainer_2": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", + "unverified_sessions_list_description": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", + "url": "URL", + "verified_session": "Verified session", + "verified_sessions": "Verified sessions", + "verified_sessions_explainer_1": "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.", + "verified_sessions_explainer_2": "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.", + "verified_sessions_list_description": "For best security, sign out from any session that you don't recognize or use anymore.", + "verify_session": "Verify session", + "web_session": "Web session" + }, + "show_avatar_changes": "Show profile picture changes", + "show_breadcrumbs": "Show shortcuts to recently viewed rooms above the room list", + "show_chat_effects": "Show chat effects (animations when receiving e.g. confetti)", + "show_displayname_changes": "Show display name changes", + "show_join_leave": "Show join/leave messages (invites/removes/bans unaffected)", + "show_nsfw_content": "Show NSFW content", + "show_read_receipts": "Show read receipts sent by other users", + "show_redaction_placeholder": "Show a placeholder for removed messages", + "show_stickers_button": "Show stickers button", + "show_typing_notifications": "Show typing notifications", + "showbold": "Show all activity in the room list (dots or number of unread messages)", + "sidebar": { + "dialog_title": "Settings: Sidebar", + "metaspaces_favourites_description": "Group all your favourite rooms and people in one place.", + "metaspaces_home_all_rooms": "Show all rooms", + "metaspaces_home_all_rooms_description": "Show all your rooms in Home, even if they're in a space.", + "metaspaces_home_description": "Home is useful for getting an overview of everything.", + "metaspaces_orphans": "Rooms outside of a space", + "metaspaces_orphans_description": "Group all your rooms that aren't part of a space in one place.", + "metaspaces_people_description": "Group all your people in one place.", + "metaspaces_subsection": "Spaces to show", + "metaspaces_video_rooms": "Video rooms and conferences", + "metaspaces_video_rooms_description": "Group all private video rooms and conferences.", + "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", + "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", + "title": "Sidebar" }, + "start_automatically": "Start automatically after system login", + "tac_only_notifications": "Only show notifications in the thread activity centre", + "use_12_hour_format": "Show timestamps in 12 hour format (e.g. 2:30pm)", + "use_command_enter_send_message": "Use Command + Enter to send a message", + "use_command_f_search": "Use Command + F to search timeline", + "use_control_enter_send_message": "Use Ctrl + Enter to send a message", + "use_control_f_search": "Use Ctrl + F to search timeline", "voip": { - "already_in_call": "Already in call", - "already_in_call_person": "You're already in a call with this person.", - "answered_elsewhere": "Answered Elsewhere", - "answered_elsewhere_description": "The call was answered on another device.", - "call_failed": "Call Failed", - "call_failed_description": "The call could not be established", - "call_failed_media": "Call failed because webcam or microphone could not be accessed. Check that:", - "call_failed_media_applications": "No other application is using the webcam", - "call_failed_media_connected": "A microphone and webcam are plugged in and set up correctly", - "call_failed_media_permissions": "Permission is granted to use the webcam", - "call_failed_microphone": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", - "call_held": "%(peerName)s held the call", - "call_held_resume": "You held the call Resume", - "call_held_switch": "You held the call Switch", - "call_toast_unknown_room": "Unknown room", - "camera_disabled": "Your camera is turned off", - "camera_enabled": "Your camera is still enabled", - "cannot_call_yourself_description": "You cannot place a call with yourself.", - "close_lobby": "Close lobby", - "connecting": "Connecting", - "connection_lost": "Connectivity to the server has been lost", - "connection_lost_description": "You cannot place calls without a connection to the server.", - "consulting": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", - "default_device": "Default Device", - "dial": "Dial", - "dialpad": "Dialpad", - "disable_camera": "Turn off camera", - "disable_microphone": "Mute microphone", - "disabled_no_one_here": "There's no one here to call", - "disabled_no_perms_start_video_call": "You do not have permission to start video calls", - "disabled_no_perms_start_voice_call": "You do not have permission to start voice calls", - "disabled_ongoing_call": "Ongoing call", - "element_call": "Element Call", - "enable_camera": "Turn on camera", - "enable_microphone": "Unmute microphone", - "expand": "Return to call", - "get_call_link": "Share call link", - "hangup": "Hangup", - "hide_sidebar_button": "Hide sidebar", - "input_devices": "Input devices", - "jitsi_call": "Jitsi Conference", - "join_button_tooltip_call_full": "Sorry — this call is currently full", - "join_button_tooltip_connecting": "Connecting", - "legacy_call": "Legacy Call", - "maximise": "Fill screen", - "maximise_call": "Maximise call", - "metaspace_video_rooms": { - "conference_room_section": "Conferences" - }, - "minimise_call": "Minimise call", - "misconfigured_server": "Call failed due to misconfigured server", - "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "misconfigured_server_fallback": "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "misconfigured_server_fallback_accept": "Try using %(server)s", - "more_button": "More", - "msisdn_lookup_failed": "Unable to look up phone number", - "msisdn_lookup_failed_description": "There was an error looking up the phone number", - "msisdn_transfer_failed": "Unable to transfer call", - "n_people_joined": { - "one": "%(count)s person joined", - "other": "%(count)s people joined" - }, - "no_audio_input_description": "We didn't find a microphone on your device. Please check your settings and try again.", - "no_audio_input_title": "No microphone found", - "no_media_perms_description": "You may need to manually permit %(brand)s to access your microphone/webcam", - "no_media_perms_title": "No media permissions", - "no_permission_conference": "Permission Required", - "no_permission_conference_description": "You do not have permission to start a conference call in this room", - "on_hold": "%(name)s on hold", - "output_devices": "Output devices", - "screenshare_monitor": "Share entire screen", - "screenshare_title": "Share content", - "screenshare_window": "Application window", - "show_sidebar_button": "Show sidebar", - "silence": "Silence call", - "silenced": "Notifications silenced", - "start_screenshare": "Start sharing your screen", - "stop_screenshare": "Stop sharing your screen", - "too_many_calls": "Too Many Calls", - "too_many_calls_description": "You've reached the maximum number of simultaneous calls.", - "transfer_consult_first_label": "Consult first", - "transfer_failed": "Transfer Failed", - "transfer_failed_description": "Failed to transfer call", - "unable_to_access_audio_input_description": "We were unable to access your microphone. Please check your browser settings and try again.", - "unable_to_access_audio_input_title": "Unable to access your microphone", - "unable_to_access_media": "Unable to access webcam / microphone", - "unable_to_access_microphone": "Unable to access microphone", - "unknown_caller": "Unknown caller", - "unknown_person": "unknown person", - "unsilence": "Sound on", - "unsupported": "Calls are unsupported", - "unsupported_browser": "You cannot place calls in this browser.", - "user_busy": "User Busy", - "user_busy_description": "The user you called is busy.", - "user_is_presenting": "%(sharerName)s is presenting", - "video_call": "Video call", - "video_call_started": "Video call started", - "video_call_using": "Video call using:", - "voice_call": "Voice call", - "you_are_presenting": "You are presenting" - }, - "web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s", - "welcome_to_element": "Welcome to Element", - "widget": { - "added_by": "Widget added by", - "capabilities_dialog": { - "content_starting_text": "This widget would like to:", - "decline_all_permission": "Decline All", - "remember_Selection": "Remember my selection for this widget", - "title": "Approve widget permissions" - }, - "capability": { - "always_on_screen_generic": "Remain on your screen while running", - "always_on_screen_viewing_another_room": "Remain on your screen when viewing another room, when running", - "any_room": "The above, but in any room you are joined or invited to as well", - "byline_empty_state_key": "with an empty state key", - "byline_state_key": "with state key %(stateKey)s", - "capability": "The %(capability)s capability", - "change_avatar_active_room": "Change the avatar of your active room", - "change_avatar_this_room": "Change the avatar of this room", - "change_name_active_room": "Change the name of your active room", - "change_name_this_room": "Change the name of this room", - "change_topic_active_room": "Change the topic of your active room", - "change_topic_this_room": "Change the topic of this room", - "receive_membership_active_room": "See when people join, leave, or are invited to your active room", - "receive_membership_this_room": "See when people join, leave, or are invited to this room", - "remove_ban_invite_leave_active_room": "Remove, ban, or invite people to your active room, and make you leave", - "remove_ban_invite_leave_this_room": "Remove, ban, or invite people to this room, and make you leave", - "see_avatar_change_active_room": "See when the avatar changes in your active room", - "see_avatar_change_this_room": "See when the avatar changes in this room", - "see_event_type_sent_active_room": "See %(eventType)s events posted to your active room", - "see_event_type_sent_this_room": "See %(eventType)s events posted to this room", - "see_images_sent_active_room": "See images posted to your active room", - "see_images_sent_this_room": "See images posted to this room", - "see_messages_sent_active_room": "See messages posted to your active room", - "see_messages_sent_this_room": "See messages posted to this room", - "see_msgtype_sent_active_room": "See %(msgtype)s messages posted to your active room", - "see_msgtype_sent_this_room": "See %(msgtype)s messages posted to this room", - "see_name_change_active_room": "See when the name changes in your active room", - "see_name_change_this_room": "See when the name changes in this room", - "see_sent_emotes_active_room": "See emotes posted to your active room", - "see_sent_emotes_this_room": "See emotes posted to this room", - "see_sent_files_active_room": "See general files posted to your active room", - "see_sent_files_this_room": "See general files posted to this room", - "see_sticker_posted_active_room": "See when anyone posts a sticker to your active room", - "see_sticker_posted_this_room": "See when a sticker is posted in this room", - "see_text_messages_sent_active_room": "See text messages posted to your active room", - "see_text_messages_sent_this_room": "See text messages posted to this room", - "see_topic_change_active_room": "See when the topic changes in your active room", - "see_topic_change_this_room": "See when the topic changes in this room", - "see_videos_sent_active_room": "See videos posted to your active room", - "see_videos_sent_this_room": "See videos posted to this room", - "send_emotes_active_room": "Send emotes as you in your active room", - "send_emotes_this_room": "Send emotes as you in this room", - "send_event_type_active_room": "Send %(eventType)s events as you in your active room", - "send_event_type_this_room": "Send %(eventType)s events as you in this room", - "send_files_active_room": "Send general files as you in your active room", - "send_files_this_room": "Send general files as you in this room", - "send_images_active_room": "Send images as you in your active room", - "send_images_this_room": "Send images as you in this room", - "send_messages_active_room": "Send messages as you in your active room", - "send_messages_this_room": "Send messages as you in this room", - "send_msgtype_active_room": "Send %(msgtype)s messages as you in your active room", - "send_msgtype_this_room": "Send %(msgtype)s messages as you in this room", - "send_stickers_active_room": "Send stickers into your active room", - "send_stickers_active_room_as_you": "Send stickers to your active room as you", - "send_stickers_this_room": "Send stickers into this room", - "send_stickers_this_room_as_you": "Send stickers to this room as you", - "send_text_messages_active_room": "Send text messages as you in your active room", - "send_text_messages_this_room": "Send text messages as you in this room", - "send_videos_active_room": "Send videos as you in your active room", - "send_videos_this_room": "Send videos as you in this room", - "specific_room": "The above, but in as well", - "switch_room": "Change which room you're viewing", - "switch_room_message_user": "Change which room, message, or user you're viewing" - }, - "close_to_view_right_panel": "Close this widget to view it in this panel", - "context_menu": { - "delete": "Delete widget", - "delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", - "move_left": "Move left", - "move_right": "Move right", - "remove": "Remove for everyone", - "revoke": "Revoke permissions", - "screenshot": "Take a picture", - "start_audio_stream": "Start audio stream" - }, - "cookie_warning": "This widget may use cookies.", - "error_hangup_description": "You were disconnected from the call. (Error: %(message)s)", - "error_hangup_title": "Connection lost", - "error_loading": "Error loading Widget", - "error_mixed_content": "Error - Mixed content", - "error_need_invite_permission": "You need to be able to invite users to do that.", - "error_need_kick_permission": "You need to be able to kick users to do that.", - "error_need_to_be_logged_in": "You need to be logged in.", - "error_unable_start_audio_stream_description": "Unable to start audio streaming.", - "error_unable_start_audio_stream_title": "Failed to start livestream", - "modal_data_warning": "Data on this screen is shared with %(widgetDomain)s", - "modal_title_default": "Modal Widget", - "no_name": "Unknown App", - "open_id_permissions_dialog": { - "remember_selection": "Remember this", - "starting_text": "The widget will verify your user ID, but won't be able to perform actions for you:", - "title": "Allow this widget to verify your identity" - }, - "popout": "Popout widget", - "set_room_layout": "Set layout for everyone", - "shared_data_avatar": "Your profile picture URL", - "shared_data_device_id": "Your device ID", - "shared_data_lang": "Your language", - "shared_data_mxid": "Your user ID", - "shared_data_name": "Your display name", - "shared_data_room_id": "Room ID", - "shared_data_theme": "Your theme", - "shared_data_url": "%(brand)s URL", - "shared_data_warning": "Using this widget may share data with %(widgetDomain)s.", - "shared_data_warning_im": "Using this widget may share data with %(widgetDomain)s & your integration manager.", - "shared_data_widget_id": "Widget ID", - "unencrypted_warning": "Widgets do not use message encryption.", - "unmaximise": "Un-maximise", - "unpin_to_view_right_panel": "Unpin this widget to view it in this panel" - }, - "zxcvbn": { - "suggestions": { - "allUppercase": "All-uppercase is almost as easy to guess as all-lowercase", - "anotherWord": "Add another word or two. Uncommon words are better.", - "associatedYears": "Avoid years that are associated with you", - "capitalization": "Capitalization doesn't help very much", - "dates": "Avoid dates and years that are associated with you", - "l33t": "Predictable substitutions like '@' instead of 'a' don't help very much", - "longerKeyboardPattern": "Use a longer keyboard pattern with more turns", - "noNeed": "No need for symbols, digits, or uppercase letters", - "pwned": "If you use this password elsewhere, you should change it.", - "recentYears": "Avoid recent years", - "repeated": "Avoid repeated words and characters", - "reverseWords": "Reversed words aren't much harder to guess", - "sequences": "Avoid sequences", - "useWords": "Use a few words, avoid common phrases" - }, - "warnings": { - "common": "This is a very common password", - "commonNames": "Common names and surnames are easy to guess", - "dates": "Dates are often easy to guess", - "extendedRepeat": "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"", - "keyPattern": "Short keyboard patterns are easy to guess", - "namesByThemselves": "Names and surnames by themselves are easy to guess", - "pwned": "Your password was exposed by a data breach on the Internet.", - "recentYears": "Recent years are easy to guess", - "sequences": "Sequences like abc or 6543 are easy to guess", - "similarToCommon": "This is similar to a commonly used password", - "simpleRepeat": "Repeats like \"aaa\" are easy to guess", - "straightRow": "Straight rows of keys are easy to guess", - "topHundred": "This is a top-100 common password", - "topTen": "This is a top-10 common password", - "userInputs": "There should not be any personal or page related data.", - "wordByItself": "A word by itself is easy to guess" - } + "allow_p2p": "Allow Peer-to-Peer for 1:1 calls", + "allow_p2p_description": "When enabled, the other party might be able to see your IP address", + "audio_input_empty": "No Microphones detected", + "audio_output": "Audio Output", + "audio_output_empty": "No Audio Outputs detected", + "auto_gain_control": "Automatic gain control", + "connection_section": "Connection", + "dialog_title": "Settings: Voice & Video", + "echo_cancellation": "Echo cancellation", + "enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)", + "enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", + "mirror_local_feed": "Mirror local video feed", + "missing_permissions_prompt": "Missing media permissions, click the button below to request.", + "noise_suppression": "Noise suppression", + "request_permissions": "Request media permissions", + "title": "Voice & Video", + "video_input_empty": "No Webcams detected", + "video_section": "Video settings", + "voice_agc": "Automatically adjust the microphone volume", + "voice_processing": "Voice processing", + "voice_section": "Voice settings" + }, + "warn_quit": "Warn before quitting", + "warning": "WARNING: " + }, + "share": { + "link_copied": "Link copied", + "permalink_message": "Link to selected message", + "permalink_most_recent": "Link to most recent message", + "share_call": "Conference invite link", + "share_call_subtitle": "Link for external users to join the call without a matrix account:", + "title_link": "Share Link", + "title_message": "Share Room Message", + "title_room": "Share Room", + "title_user": "Share User" + }, + "slash_command": { + "addwidget": "Adds a custom widget by URL to the room", + "addwidget_iframe_missing_src": "iframe has no src attribute", + "addwidget_invalid_protocol": "Please supply a https:// or http:// widget URL", + "addwidget_missing_url": "Please supply a widget URL or embed code", + "addwidget_no_permissions": "You cannot modify widgets in this room.", + "ban": "Bans user with given id", + "category_actions": "Actions", + "category_admin": "Admin", + "category_advanced": "Advanced", + "category_effects": "Effects", + "category_messages": "Messages", + "category_other": "Other", + "command_error": "Command error", + "converttodm": "Converts the room to a DM", + "converttoroom": "Converts the DM to a room", + "could_not_find_room": "Could not find room", + "deop": "Deops user with given id", + "devtools": "Opens the Developer Tools dialog", + "discardsession": "Forces the current outbound group session in an encrypted room to be discarded", + "error_invalid_rendering_type": "Command error: Unable to find rendering type (%(renderingType)s)", + "error_invalid_room": "Command failed: Unable to find room (%(roomId)s)", + "error_invalid_runfn": "Command error: Unable to handle slash command.", + "error_invalid_user_in_room": "Could not find user in room", + "help": "Displays list of commands with usages and descriptions", + "help_dialog_title": "Command Help", + "holdcall": "Places the call in the current room on hold", + "html": "Sends a message as html, without interpreting it as markdown", + "ignore": "Ignores a user, hiding their messages from you", + "ignore_dialog_description": "You are now ignoring %(userId)s", + "ignore_dialog_title": "Ignored user", + "invite": "Invites user with given id to current room", + "invite_3pid_needs_is_error": "Use an identity server to invite by email. Manage in Settings.", + "invite_3pid_use_default_is_title": "Use an identity server", + "invite_3pid_use_default_is_title_description": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", + "invite_failed": "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility", + "join": "Joins room with given address", + "jumptodate": "Jump to the given date in the timeline", + "jumptodate_invalid_input": "We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.", + "lenny": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message", + "me": "Displays action", + "msg": "Sends a message to the given user", + "myavatar": "Changes your profile picture in all rooms", + "myroomavatar": "Changes your profile picture in this current room only", + "myroomnick": "Changes your display nickname in the current room only", + "nick": "Changes your display nickname", + "no_active_call": "No active call in this room", + "op": "Define the power level of a user", + "part_unknown_alias": "Unrecognised room address: %(roomAlias)s", + "plain": "Sends a message as plain text, without interpreting it as markdown", + "query": "Opens chat with the given user", + "query_not_found_phone_number": "Unable to find Matrix ID for phone number", + "rageshake": "Send a bug report with logs", + "rainbow": "Sends the given message coloured as a rainbow", + "rainbowme": "Sends the given emote coloured as a rainbow", + "remove": "Removes user with given id from this room", + "roomavatar": "Changes the avatar of the current room", + "roomname": "Sets the room name", + "server_error": "Server error", + "server_error_detail": "Server unavailable, overloaded, or something else went wrong.", + "shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", + "spoiler": "Sends the given message as a spoiler", + "tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message", + "topic": "Gets or sets the room topic", + "topic_none": "This room has no topic.", + "topic_room_error": "Failed to get room topic: Unable to find room (%(roomId)s", + "tovirtual": "Switches to this room's virtual room, if it has one", + "tovirtual_not_found": "No virtual room for this room", + "unban": "Unbans user with given ID", + "unflip": "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message", + "unholdcall": "Takes the call in the current room off hold", + "unignore": "Stops ignoring a user, showing their messages going forward", + "unignore_dialog_description": "You are no longer ignoring %(userId)s", + "unignore_dialog_title": "Unignored user", + "unknown_command": "Unknown Command", + "unknown_command_button": "Send as message", + "unknown_command_detail": "Unrecognised command: %(commandText)s", + "unknown_command_help": "You can use /help to list available commands. Did you mean to send this as a message?", + "unknown_command_hint": "Hint: Begin your message with // to start it with a slash.", + "upgraderoom": "Upgrades a room to a new version", + "upgraderoom_permission_error": "You do not have the required permissions to use this command.", + "usage": "Usage", + "view": "Views room with given address", + "whois": "Displays information about a user" + }, + "sliding_sync_legacy_no_longer_supported": "Legacy sliding sync is no longer supported: please log out and back in to enable the new sliding sync flag", + "space": { + "add_existing_room_space": { + "create": "Want to add a new room instead?", + "create_prompt": "Create a new room", + "dm_heading": "Direct Messages", + "error_heading": "Not all selected were added", + "progress_text": { + "one": "Adding room...", + "other": "Adding rooms... (%(progress)s out of %(count)s)" + }, + "space_dropdown_label": "Space selection", + "space_dropdown_title": "Add existing rooms", + "subspace_moved_note": "Adding spaces has moved." + }, + "add_existing_subspace": { + "create_button": "Create a new space", + "create_prompt": "Want to add a new space instead?", + "filter_placeholder": "Search for spaces", + "space_dropdown_title": "Add existing space" + }, + "context_menu": { + "devtools_open_timeline": "See room timeline (devtools)", + "explore": "Explore rooms", + "home": "Space home", + "manage_and_explore": "Manage & explore rooms", + "options": "Space options" + }, + "failed_load_rooms": "Failed to load list of rooms.", + "failed_remove_rooms": "Failed to remove some rooms. Try again later", + "incompatible_server_hierarchy": "Your server does not support showing space hierarchies.", + "invite": "Invite people", + "invite_description": "Invite with email or username", + "invite_link": "Share invite link", + "joining_space": "Joining", + "landing_welcome": "Welcome to ", + "leave_dialog_action": "Leave space", + "leave_dialog_description": "You are about to leave .", + "leave_dialog_only_admin_room_warning": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.", + "leave_dialog_only_admin_warning": "You're the only admin of this space. Leaving it will mean no one has control over it.", + "leave_dialog_option_all": "Leave all rooms", + "leave_dialog_option_intro": "Would you like to leave the rooms in this space?", + "leave_dialog_option_none": "Don't leave any rooms", + "leave_dialog_option_specific": "Leave some rooms", + "leave_dialog_public_rejoin_warning": "You won't be able to rejoin unless you are re-invited.", + "leave_dialog_title": "Leave %(spaceName)s", + "mark_suggested": "Mark as suggested", + "no_search_result_hint": "You may want to try a different search or check for typos.", + "preferences": { + "sections_section": "Sections to show", + "show_people_in_space": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s." + }, + "room_filter_placeholder": "Search for rooms", + "search_children": "Search %(spaceName)s", + "search_placeholder": "Search names and descriptions", + "select_room_below": "Select a room below first", + "share_public": "Share your public space", + "suggested": "Suggested", + "suggested_tooltip": "This room is suggested as a good one to join", + "title_when_query_available": "Results", + "title_when_query_unavailable": "Rooms and spaces", + "unmark_suggested": "Mark as not suggested", + "user_lacks_permission": "You don't have permission" + }, + "space_settings": { + "title": "Settings - %(spaceName)s" + }, + "spaces": { + "error_no_permission_add_room": "You do not have permissions to add rooms to this space", + "error_no_permission_add_space": "You do not have permissions to add spaces to this space", + "error_no_permission_create_room": "You do not have permissions to create new rooms in this space", + "error_no_permission_invite": "You do not have permissions to invite people to this space" + }, + "spotlight": { + "public_rooms": { + "network_dropdown_add_dialog_description": "Enter the name of a new server you want to explore.", + "network_dropdown_add_dialog_placeholder": "Server name", + "network_dropdown_add_dialog_title": "Add a new server", + "network_dropdown_add_server_option": "Add new server…", + "network_dropdown_available_invalid": "Can't find this server or its room list", + "network_dropdown_available_invalid_forbidden": "You are not allowed to view this server's rooms list", + "network_dropdown_available_valid": "Looks good", + "network_dropdown_remove_server_adornment": "Remove server “%(roomServer)s”", + "network_dropdown_required_invalid": "Enter a server name", + "network_dropdown_selected_label": "Show: Matrix rooms", + "network_dropdown_selected_label_instance": "Show: %(instance)s rooms (%(server)s)", + "network_dropdown_your_server_description": "Your server" + } + }, + "spotlight_dialog": { + "cant_find_person_helpful_hint": "If you can't see who you're looking for, send them your invite link.", + "cant_find_room_helpful_hint": "If you can't find the room you're looking for, ask for an invite or create a new room.", + "copy_link_text": "Copy invite link", + "count_of_members": { + "one": "%(count)s Member", + "other": "%(count)s Members" + }, + "create_new_room_button": "Create new room", + "failed_querying_public_rooms": "Failed to query public rooms", + "failed_querying_public_spaces": "Failed to query public spaces", + "group_chat_section_title": "Other options", + "heading_with_query": "Use \"%(query)s\" to search", + "heading_without_query": "Search for", + "join_button_text": "Join %(roomAddress)s", + "keyboard_scroll_hint": "Use to scroll", + "message_search_section_title": "Other searches", + "other_rooms_in_space": "Other rooms in %(spaceName)s", + "public_rooms_label": "Public rooms", + "public_spaces_label": "Public spaces", + "recent_searches_section_title": "Recent searches", + "recently_viewed_section_title": "Recently viewed", + "remove_filter": "Remove search filter for %(filter)s", + "result_may_be_hidden_privacy_warning": "Some results may be hidden for privacy", + "result_may_be_hidden_warning": "Some results may be hidden", + "search_dialog": "Search Dialog", + "search_messages_hint": "To search messages, look for this icon at the top of a room ", + "spaces_title": "Spaces you're in", + "start_group_chat_button": "Start a group chat" + }, + "stickers": { + "empty": "You don't currently have any stickerpacks enabled", + "empty_add_prompt": "Add some now" + }, + "terms": { + "column_document": "Document", + "column_service": "Service", + "column_summary": "Summary", + "identity_server_no_terms_description_1": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", + "identity_server_no_terms_description_2": "Only continue if you trust the owner of the server.", + "identity_server_no_terms_title": "Identity server has no terms of service", + "inline_intro_text": "Accept to continue:", + "integration_manager": "Use bots, bridges, widgets and sticker packs", + "intro": "To continue you need to accept the terms of this service.", + "summary_identity_server_1": "Find others by phone or email", + "summary_identity_server_2": "Be found by phone or email", + "tac_button": "Review terms and conditions", + "tac_description": "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.", + "tac_title": "Terms and Conditions", + "tos": "Terms of Service" + }, + "theme": { + "light_high_contrast": "Light high contrast", + "match_system": "Match system" + }, + "thread_view_back_action_label": "Back to thread", + "threads": { + "all_threads": "All threads", + "all_threads_description": "Shows all threads from current room", + "count_of_reply": { + "one": "%(count)s reply", + "other": "%(count)s replies" + }, + "empty_description": "Use “%(replyInThread)s” when hovering over a message.", + "empty_title": "Threads help keep your conversations on-topic and easy to track.", + "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", + "mark_all_read": "Mark all as read", + "my_threads": "My threads", + "my_threads_description": "Shows all threads you've participated in", + "open_thread": "Open thread", + "show_thread_filter": "Show:" + }, + "threads_activity_centre": { + "header": "Threads activity", + "no_rooms_with_threads_notifs": "You don't have rooms with thread notifications yet.", + "no_rooms_with_unread_threads": "You don't have rooms with unread threads yet.", + "release_announcement_description": "Threads notifications have moved, find them here from now on.", + "release_announcement_header": "Threads Activity Centre" + }, + "time": { + "about_day_ago": "about a day ago", + "about_hour_ago": "about an hour ago", + "about_minute_ago": "about a minute ago", + "date_at_time": "%(date)s at %(time)s", + "few_seconds_ago": "a few seconds ago", + "hours_minutes_seconds_left": "%(hours)sh %(minutes)sm %(seconds)ss left", + "in_about_day": "about a day from now", + "in_about_hour": "about an hour from now", + "in_about_minute": "about a minute from now", + "in_few_seconds": "a few seconds from now", + "in_n_days": "%(num)s days from now", + "in_n_hours": "%(num)s hours from now", + "in_n_minutes": "%(num)s minutes from now", + "left": "%(timeRemaining)s left", + "minutes_seconds_left": "%(minutes)sm %(seconds)ss left", + "n_days_ago": "%(num)s days ago", + "n_hours_ago": "%(num)s hours ago", + "n_minutes_ago": "%(num)s minutes ago", + "seconds_left": "%(seconds)ss left", + "short_days": "%(value)sd", + "short_days_hours_minutes_seconds": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", + "short_hours": "%(value)sh", + "short_hours_minutes_seconds": "%(hours)sh %(minutes)sm %(seconds)ss", + "short_minutes": "%(value)sm", + "short_minutes_seconds": "%(minutes)sm %(seconds)ss", + "short_seconds": "%(value)ss" + }, + "timeline": { + "context_menu": { + "collapse_reply_thread": "Collapse reply thread", + "external_url": "Source URL", + "open_in_osm": "Open in OpenStreetMap", + "report": "Report", + "resent_unsent_reactions": "Resend %(unsentCount)s reaction(s)", + "show_url_preview": "Show preview", + "view_related_event": "View related event", + "view_source": "View source" + }, + "creation_summary_dm": "%(creator)s created this DM.", + "creation_summary_room": "%(creator)s created and configured the room.", + "decryption_failure": { + "blocked": "The sender has blocked you from receiving this message because your device is unverified", + "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_user_not_joined": "You don't have access to this message", + "sender_identity_previously_verified": "Sender's verified identity has changed", + "sender_unsigned_device": "Sent from an insecure device.", + "unable_to_decrypt": "Unable to decrypt message" + }, + "disambiguated_profile": "%(displayName)s (%(matrixId)s)", + "download_action_decrypting": "Decrypting", + "download_action_downloading": "Downloading", + "download_failed": "Download failed", + "download_failed_description": "An error occurred while downloading this file", + "e2e_state": "State of the end-to-end encryption", + "edits": { + "tooltip_label": "Edited at %(date)s. Click to view edits.", + "tooltip_sub": "Click to view edits", + "tooltip_title": "Edited at %(date)s" + }, + "error_no_renderer": "This event could not be displayed", + "error_rendering_message": "Can't load this message", + "historical_messages_unavailable": "You can't see earlier messages", + "in_room_name": " in %(room)s", + "io.element.widgets.layout": "%(senderName)s has updated the room layout", + "late_event_separator": "Originally sent %(dateTime)s", + "load_error": { + "no_permission": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", + "title": "Failed to load timeline position", + "unable_to_find": "Tried to load a specific point in this room's timeline, but was unable to find it." + }, + "m.audio": { + "error_downloading_audio": "Error downloading audio", + "error_processing_audio": "Error processing audio message", + "error_processing_voice_message": "Error processing voice message", + "unnamed_audio": "Unnamed audio" + }, + "m.beacon_info": { + "view_live_location": "View live location" + }, + "m.call": { + "video_call_ended": "Video call ended", + "video_call_started": "Video call started in %(roomName)s.", + "video_call_started_text": "%(name)s started a video call", + "video_call_started_unsupported": "Video call started in %(roomName)s. (not supported by this browser)" + }, + "m.call.hangup": { + "dm": "Call ended" + }, + "m.call.invite": { + "answered_elsewhere": "Answered elsewhere", + "call_back_prompt": "Call back", + "declined": "Call declined", + "failed_connect_media": "Could not connect media", + "failed_connection": "Connection failed", + "failed_opponent_media": "Their device couldn't start the camera or microphone", + "missed_call": "Missed call", + "no_answer": "No answer", + "unknown_error": "An unknown error occurred", + "unknown_failure": "Unknown failure: %(reason)s", + "unknown_state": "The call is in an unknown state!", + "video_call": "%(senderName)s placed a video call.", + "video_call_unsupported": "%(senderName)s placed a video call. (not supported by this browser)", + "voice_call": "%(senderName)s placed a voice call.", + "voice_call_unsupported": "%(senderName)s placed a voice call. (not supported by this browser)" + }, + "m.file": { + "error_decrypting": "Error decrypting attachment", + "error_invalid": "Invalid file" + }, + "m.image": { + "error": "Unable to show image due to error", + "error_decrypting": "Error decrypting image", + "error_downloading": "Error downloading image", + "sent": "%(senderDisplayName)s sent an image.", + "show_image": "Show image" + }, + "m.key.verification.request": { + "user_wants_to_verify": "%(name)s wants to verify", + "you_started": "You sent a verification request" + }, + "m.location": { + "full": "%(senderName)s has shared their location", + "location": "Shared a location: ", + "self_location": "Shared their location: " + }, + "m.poll": { + "count_of_votes": { + "one": "%(count)s vote", + "other": "%(count)s votes" + } + }, + "m.poll.end": { + "ended": "Ended a poll", + "sender_ended": "%(senderName)s has ended a poll" + }, + "m.poll.start": "%(senderName)s has started a poll - %(pollQuestion)s", + "m.room.avatar": { + "changed": "%(senderDisplayName)s changed the room avatar.", + "changed_img": "%(senderDisplayName)s changed the room avatar to ", + "lightbox_title": "%(senderDisplayName)s changed the avatar for %(roomName)s", + "removed": "%(senderDisplayName)s removed the room avatar." + }, + "m.room.canonical_alias": { + "alt_added": { + "one": "%(senderName)s added alternative address %(addresses)s for this room.", + "other": "%(senderName)s added the alternative addresses %(addresses)s for this room." + }, + "alt_removed": { + "one": "%(senderName)s removed alternative address %(addresses)s for this room.", + "other": "%(senderName)s removed the alternative addresses %(addresses)s for this room." + }, + "changed": "%(senderName)s changed the addresses for this room.", + "changed_alternative": "%(senderName)s changed the alternative addresses for this room.", + "changed_main_and_alternative": "%(senderName)s changed the main and alternative addresses for this room.", + "removed": "%(senderName)s removed the main address for this room.", + "set": "%(senderName)s set the main address for this room to %(address)s." + }, + "m.room.create": { + "continuation": "This room is a continuation of another conversation.", + "see_older_messages": "Click here to see older messages.", + "unknown_predecessor": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.", + "unknown_predecessor_guess_server": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:" + }, + "m.room.encryption": { + "disable_attempt": "Ignored attempt to disable encryption", + "disabled": "Encryption not enabled", + "enabled": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.", + "enabled_dm": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.", + "enabled_local": "Messages in this chat will be end-to-end encrypted.", + "parameters_changed": "Some encryption parameters have been changed.", + "unsupported": "The encryption used by this room isn't supported." + }, + "m.room.guest_access": { + "can_join": "%(senderDisplayName)s has allowed guests to join the room.", + "forbidden": "%(senderDisplayName)s has prevented guests from joining the room.", + "unknown": "%(senderDisplayName)s changed guest access to %(rule)s" + }, + "m.room.history_visibility": { + "invited": "%(senderName)s made future room history visible to all room members, from the point they are invited.", + "joined": "%(senderName)s made future room history visible to all room members, from the point they joined.", + "shared": "%(senderName)s made future room history visible to all room members.", + "unknown": "%(senderName)s made future room history visible to unknown (%(visibility)s).", + "world_readable": "%(senderName)s made future room history visible to anyone." + }, + "m.room.join_rules": { + "invite": "%(senderDisplayName)s made the room invite only.", + "knock": "%(senderDisplayName)s changed the join rule to ask to join.", + "public": "%(senderDisplayName)s made the room public to whoever knows the link.", + "restricted": "%(senderDisplayName)s changed who can join this room.", + "restricted_settings": "%(senderDisplayName)s changed who can join this room. View settings.", + "unknown": "%(senderDisplayName)s changed the join rule to %(rule)s" + }, + "m.room.member": { + "accepted_3pid_invite": "%(targetName)s accepted the invitation for %(displayName)s", + "accepted_invite": "%(targetName)s accepted an invitation", + "ban": "%(senderName)s banned %(targetName)s", + "ban_reason": "%(senderName)s banned %(targetName)s: %(reason)s", + "change_avatar": "%(senderName)s changed their profile picture", + "change_name": "%(oldDisplayName)s changed their display name to %(displayName)s", + "change_name_avatar": "%(oldDisplayName)s changed their display name and profile picture", + "invite": "%(senderName)s invited %(targetName)s", + "join": "%(targetName)s joined the room", + "kick": "%(senderName)s removed %(targetName)s", + "kick_reason": "%(senderName)s removed %(targetName)s: %(reason)s", + "left": "%(targetName)s left the room", + "left_reason": "%(targetName)s left the room: %(reason)s", + "no_change": "%(senderName)s made no change", + "reject_invite": "%(targetName)s rejected the invitation", + "reject_invite_reason": "%(targetName)s rejected the invitation: %(reason)s", + "remove_avatar": "%(senderName)s removed their profile picture", + "remove_name": "%(senderName)s removed their display name (%(oldDisplayName)s)", + "set_avatar": "%(senderName)s set a profile picture", + "set_name": "%(senderName)s set their display name to %(displayName)s", + "unban": "%(senderName)s unbanned %(targetName)s", + "withdrew_invite": "%(senderName)s withdrew %(targetName)s's invitation", + "withdrew_invite_reason": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s" + }, + "m.room.name": { + "change": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.", + "remove": "%(senderDisplayName)s removed the room name.", + "set": "%(senderDisplayName)s changed the room name to %(roomName)s." + }, + "m.room.pinned_events": { + "changed": "%(senderName)s changed the pinned messages for the room.", + "changed_link": "%(senderName)s changed the pinned messages for the room.", + "pinned": "%(senderName)s pinned a message to this room. See all pinned messages.", + "pinned_link": "%(senderName)s pinned a message to this room. See all pinned messages.", + "unpinned": "%(senderName)s unpinned a message from this room. See all pinned messages.", + "unpinned_link": "%(senderName)s unpinned a message from this room. See all pinned messages." + }, + "m.room.power_levels": { + "changed": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", + "user_from_to": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s" + }, + "m.room.server_acl": { + "all_servers_banned": "🎉 All servers are banned from participating! This room can no longer be used.", + "changed": "%(senderDisplayName)s changed the server ACLs for this room.", + "set": "%(senderDisplayName)s set the server ACLs for this room." + }, + "m.room.third_party_invite": { + "revoked": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", + "sent": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room." + }, + "m.room.tombstone": "%(senderDisplayName)s upgraded this room.", + "m.room.topic": { + "changed": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", + "removed": "%(senderDisplayName)s removed the topic." + }, + "m.sticker": "%(senderDisplayName)s sent a sticker.", + "m.video": { + "error_decrypting": "Error decrypting video" + }, + "m.widget": { + "added": "%(widgetName)s widget added by %(senderName)s", + "jitsi_ended": "Video conference ended by %(senderName)s", + "jitsi_join_right_prompt": "Join the conference from the room information card on the right", + "jitsi_join_top_prompt": "Join the conference at the top of this room", + "jitsi_started": "Video conference started by %(senderName)s", + "jitsi_updated": "Video conference updated by %(senderName)s", + "modified": "%(widgetName)s widget modified by %(senderName)s", + "removed": "%(widgetName)s widget removed by %(senderName)s" + }, + "mab": { + "collapse_reply_chain": "Collapse quotes", + "copy_link_thread": "Copy link to thread", + "expand_reply_chain": "Expand quotes", + "label": "Message Actions", + "view_in_room": "View in room" + }, + "message_timestamp_received_at": "Received at: %(dateTime)s", + "message_timestamp_sent_at": "Sent at: %(dateTime)s", + "mjolnir": { + "changed_rule_glob": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", + "changed_rule_rooms": "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", + "changed_rule_servers": "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", + "changed_rule_users": "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", + "created_rule": "%(senderName)s created a ban rule matching %(glob)s for %(reason)s", + "created_rule_rooms": "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", + "created_rule_servers": "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", + "created_rule_users": "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", + "message_hidden": "You have ignored this user, so their message is hidden. Show anyways.", + "removed_rule": "%(senderName)s removed a ban rule matching %(glob)s", + "removed_rule_rooms": "%(senderName)s removed the rule banning rooms matching %(glob)s", + "removed_rule_servers": "%(senderName)s removed the rule banning servers matching %(glob)s", + "removed_rule_users": "%(senderName)s removed the rule banning users matching %(glob)s", + "updated_invalid_rule": "%(senderName)s updated an invalid ban rule", + "updated_rule": "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", + "updated_rule_rooms": "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", + "updated_rule_servers": "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", + "updated_rule_users": "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s" + }, + "no_permission_messages_before_invite": "You don't have permission to view messages from before you were invited.", + "no_permission_messages_before_join": "You don't have permission to view messages from before you joined.", + "pending_moderation": "Message pending moderation", + "pending_moderation_reason": "Message pending moderation: %(reason)s", + "reactions": { + "add_reaction_prompt": "Add reaction", + "custom_reaction_fallback_label": "Custom reaction", + "label": "%(reactors)s reacted with %(content)s", + "tooltip_caption": "reacted with %(shortName)s" + }, + "read_receipt_title": { + "one": "Seen by %(count)s person", + "other": "Seen by %(count)s people" + }, + "read_receipts_label": "Read receipts", + "redacted": { + "tooltip": "Message deleted on %(date)s" + }, + "redaction": "Message deleted by %(name)s", + "reply": { + "error_loading": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", + "in_reply_to": "In reply to ", + "in_reply_to_for_export": "In reply to this message" + }, + "scalar_starter_link": { + "dialog_description": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", + "dialog_title": "Add an Integration" + }, + "self_redaction": "Message deleted", + "send_state_encrypting": "Encrypting your message…", + "send_state_failed": "Failed to send", + "send_state_sending": "Sending your message…", + "send_state_sent": "Your message was sent", + "summary": { + "banned": { + "one": "was banned", + "other": "was banned %(count)s times" + }, + "banned_multiple": { + "one": "were banned", + "other": "were banned %(count)s times" + }, + "changed_avatar": { + "one": "%(oneUser)schanged their profile picture", + "other": "%(oneUser)schanged their profile picture %(count)s times" + }, + "changed_avatar_multiple": { + "one": "%(severalUsers)schanged their profile picture", + "other": "%(severalUsers)schanged their profile picture %(count)s times" + }, + "changed_name": { + "one": "%(oneUser)schanged their name", + "other": "%(oneUser)schanged their name %(count)s times" + }, + "changed_name_multiple": { + "one": "%(severalUsers)schanged their name", + "other": "%(severalUsers)schanged their name %(count)s times" + }, + "format": "%(nameList)s %(transitionList)s", + "hidden_event": { + "one": "%(oneUser)ssent a hidden message", + "other": "%(oneUser)ssent %(count)s hidden messages" + }, + "hidden_event_multiple": { + "one": "%(severalUsers)ssent a hidden message", + "other": "%(severalUsers)ssent %(count)s hidden messages" + }, + "invite_withdrawn": { + "one": "%(oneUser)shad their invitation withdrawn", + "other": "%(oneUser)shad their invitation withdrawn %(count)s times" + }, + "invite_withdrawn_multiple": { + "one": "%(severalUsers)shad their invitations withdrawn", + "other": "%(severalUsers)shad their invitations withdrawn %(count)s times" + }, + "invited": { + "one": "was invited", + "other": "was invited %(count)s times" + }, + "invited_multiple": { + "one": "were invited", + "other": "were invited %(count)s times" + }, + "joined": { + "one": "%(oneUser)sjoined", + "other": "%(oneUser)sjoined %(count)s times" + }, + "joined_and_left": { + "one": "%(oneUser)sjoined and left", + "other": "%(oneUser)sjoined and left %(count)s times" + }, + "joined_and_left_multiple": { + "one": "%(severalUsers)sjoined and left", + "other": "%(severalUsers)sjoined and left %(count)s times" + }, + "joined_multiple": { + "one": "%(severalUsers)sjoined", + "other": "%(severalUsers)sjoined %(count)s times" + }, + "kicked": { + "one": "was removed", + "other": "was removed %(count)s times" + }, + "kicked_multiple": { + "one": "were removed", + "other": "were removed %(count)s times" + }, + "left": { + "one": "%(oneUser)sleft", + "other": "%(oneUser)sleft %(count)s times" + }, + "left_multiple": { + "one": "%(severalUsers)sleft", + "other": "%(severalUsers)sleft %(count)s times" + }, + "no_change": { + "one": "%(oneUser)smade no changes", + "other": "%(oneUser)smade no changes %(count)s times" + }, + "no_change_multiple": { + "one": "%(severalUsers)smade no changes", + "other": "%(severalUsers)smade no changes %(count)s times" + }, + "pinned_events": { + "one": "%(oneUser)schanged the pinned messages for the room", + "other": "%(oneUser)schanged the pinned messages for the room %(count)s times" + }, + "pinned_events_multiple": { + "one": "%(severalUsers)schanged the pinned messages for the room", + "other": "%(severalUsers)schanged the pinned messages for the room %(count)s times" + }, + "redacted": { + "one": "%(oneUser)sremoved a message", + "other": "%(oneUser)sremoved %(count)s messages" + }, + "redacted_multiple": { + "one": "%(severalUsers)sremoved a message", + "other": "%(severalUsers)sremoved %(count)s messages" + }, + "rejected_invite": { + "one": "%(oneUser)srejected their invitation", + "other": "%(oneUser)srejected their invitation %(count)s times" + }, + "rejected_invite_multiple": { + "one": "%(severalUsers)srejected their invitations", + "other": "%(severalUsers)srejected their invitations %(count)s times" + }, + "rejoined": { + "one": "%(oneUser)sleft and rejoined", + "other": "%(oneUser)sleft and rejoined %(count)s times" + }, + "rejoined_multiple": { + "one": "%(severalUsers)sleft and rejoined", + "other": "%(severalUsers)sleft and rejoined %(count)s times" + }, + "server_acls": { + "one": "%(oneUser)schanged the server ACLs", + "other": "%(oneUser)schanged the server ACLs %(count)s times" + }, + "server_acls_multiple": { + "one": "%(severalUsers)schanged the server ACLs", + "other": "%(severalUsers)schanged the server ACLs %(count)s times" + }, + "unbanned": { + "one": "was unbanned", + "other": "was unbanned %(count)s times" + }, + "unbanned_multiple": { + "one": "were unbanned", + "other": "were unbanned %(count)s times" + } + }, + "thread_info_basic": "From a thread", + "typing_indicator": { + "more_users": { + "one": "%(names)s and one other is typing …", + "other": "%(names)s and %(count)s others are typing …" + }, + "one_user": "%(displayName)s is typing …", + "two_users": "%(names)s and %(lastPerson)s are typing …" + }, + "undecryptable_tooltip": "This message could not be decrypted", + "url_preview": { + "close": "Close preview", + "show_n_more": { + "one": "Show %(count)s other preview", + "other": "Show %(count)s other previews" + } + } + }, + "truncated_list_n_more": { + "other": "And %(count)s more..." + }, + "unsupported_browser": { + "description": "If you continue, some features may stop working and there is a risk that you may lose data in the future. Update your browser to continue using %(brand)s.", + "title": "%(brand)s does not support this browser" + }, + "unsupported_server_description": "This server is using an older version of Matrix. Upgrade to Matrix %(version)s to use %(brand)s without errors.", + "unsupported_server_title": "Your server is unsupported", + "update": { + "changelog": "Changelog", + "check_action": "Check for update", + "checking": "Checking for an update…", + "downloading": "Downloading update…", + "error_encountered": "Error encountered (%(errorDetail)s).", + "error_unable_load_commit": "Unable to load commit detail: %(msg)s", + "new_version_available": "New version available. Update now.", + "no_update": "No update available.", + "release_notes_toast_title": "What's New", + "see_changes_button": "What's new?", + "toast_description": "New version of %(brand)s is available", + "toast_title": "Update %(brand)s", + "unavailable": "Unavailable" + }, + "update_room_access_modal": { + "description": "To create a share link, you need to allow guests to join this room. This may make the room less secure. When you're done with the call, you can make the room private again.", + "dont_change_description": "Alternatively, you can hold the call in a separate room.", + "no_change": "I don't want to change the access level.", + "title": "Change the room access level" + }, + "upload_failed_generic": "The file '%(fileName)s' failed to upload.", + "upload_failed_size": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", + "upload_failed_title": "Upload Failed", + "upload_file": { + "cancel_all_button": "Cancel All", + "error_file_too_large": "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", + "error_files_too_large": "These files are too large to upload. The file size limit is %(limit)s.", + "error_some_files_too_large": "Some files are too large to be uploaded. The file size limit is %(limit)s.", + "error_title": "Upload Error", + "not_image": "The file you have chosen is not a valid image file.", + "title": "Upload files", + "title_progress": "Upload files (%(current)s of %(total)s)", + "upload_all_button": "Upload all", + "upload_n_others_button": { + "one": "Upload %(count)s other file", + "other": "Upload %(count)s other files" + } + }, + "user_info": { + "admin_tools_section": "Admin Tools", + "ban_button_room": "Ban from room", + "ban_button_space": "Ban from space", + "ban_room_confirm_title": "Ban from %(roomName)s", + "ban_space_everything": "Ban them from everything I'm able to", + "ban_space_specific": "Ban them from specific things I'm able to", + "deactivate_confirm_action": "Deactivate user", + "deactivate_confirm_description": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", + "deactivate_confirm_title": "Deactivate user?", + "demote_button": "Demote", + "demote_self_confirm_description_space": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.", + "demote_self_confirm_room": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", + "demote_self_confirm_title": "Demote yourself?", + "disinvite_button_room": "Disinvite from room", + "disinvite_button_room_name": "Disinvite from %(roomName)s", + "disinvite_button_space": "Disinvite from space", + "error_ban_user": "Failed to ban user", + "error_deactivate": "Failed to deactivate user", + "error_kicking_user": "Failed to remove user", + "error_mute_user": "Failed to mute user", + "error_revoke_3pid_invite_description": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.", + "error_revoke_3pid_invite_title": "Failed to revoke invite", + "ignore_button": "Ignore", + "ignore_confirm_description": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?", + "ignore_confirm_title": "Ignore %(user)s", + "invited_by": "Invited by %(sender)s", + "jump_to_rr_button": "Jump to read receipt", + "kick_button_room": "Remove from room", + "kick_button_room_name": "Remove from %(roomName)s", + "kick_button_space": "Remove from space", + "kick_button_space_everything": "Remove them from everything I'm able to", + "kick_space_specific": "Remove them from specific things I'm able to", + "kick_space_warning": "They'll still be able to access whatever you're not an admin of.", + "promote_warning": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", + "redact": { + "confirm_button": { + "one": "Remove 1 message", + "other": "Remove %(count)s messages" + }, + "confirm_description_1": { + "one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", + "other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?" + }, + "confirm_description_2": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "confirm_keep_state_explainer": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", + "confirm_keep_state_label": "Preserve system messages", + "confirm_title": "Remove recent messages by %(user)s", + "no_recent_messages_description": "Try scrolling up in the timeline to see if there are any earlier ones.", + "no_recent_messages_title": "No recent messages by %(user)s found" + }, + "redact_button": "Remove messages", + "revoke_invite": "Revoke invite", + "room_encrypted": "Messages in this room are end-to-end encrypted.", + "room_encrypted_detail": "Your messages are secured and only you and the recipient have the unique keys to unlock them.", + "room_unencrypted": "Messages in this room are not end-to-end encrypted.", + "room_unencrypted_detail": "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.", + "send_message": "Send message", + "share_button": "Share profile", + "unban_button_room": "Unban from room", + "unban_button_space": "Unban from space", + "unban_room_confirm_title": "Unban from %(roomName)s", + "unban_space_everything": "Unban them from everything I'm able to", + "unban_space_specific": "Unban them from specific things I'm able to", + "unban_space_warning": "They won't be able to access whatever you're not an admin of.", + "unignore_button": "Unignore", + "verification_unavailable": "User verification unavailable", + "verify_button": "Verify User", + "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." + }, + "user_menu": { + "link_new_device": "Link new device", + "settings": "All settings", + "switch_theme_dark": "Switch to dark mode", + "switch_theme_light": "Switch to light mode" + }, + "voip": { + "already_in_call": "Already in call", + "already_in_call_person": "You're already in a call with this person.", + "answered_elsewhere": "Answered Elsewhere", + "answered_elsewhere_description": "The call was answered on another device.", + "call_failed": "Call Failed", + "call_failed_description": "The call could not be established", + "call_failed_media": "Call failed because webcam or microphone could not be accessed. Check that:", + "call_failed_media_applications": "No other application is using the webcam", + "call_failed_media_connected": "A microphone and webcam are plugged in and set up correctly", + "call_failed_media_permissions": "Permission is granted to use the webcam", + "call_failed_microphone": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "call_held": "%(peerName)s held the call", + "call_held_resume": "You held the call Resume", + "call_held_switch": "You held the call Switch", + "call_toast_unknown_room": "Unknown room", + "camera_disabled": "Your camera is turned off", + "camera_enabled": "Your camera is still enabled", + "cannot_call_yourself_description": "You cannot place a call with yourself.", + "close_lobby": "Close lobby", + "connecting": "Connecting", + "connection_lost": "Connectivity to the server has been lost", + "connection_lost_description": "You cannot place calls without a connection to the server.", + "consulting": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + "default_device": "Default Device", + "dial": "Dial", + "dialpad": "Dialpad", + "disable_camera": "Turn off camera", + "disable_microphone": "Mute microphone", + "disabled_no_one_here": "There's no one here to call", + "disabled_no_perms_start_video_call": "You do not have permission to start video calls", + "disabled_no_perms_start_voice_call": "You do not have permission to start voice calls", + "disabled_ongoing_call": "Ongoing call", + "element_call": "Element Call", + "enable_camera": "Turn on camera", + "enable_microphone": "Unmute microphone", + "expand": "Return to call", + "get_call_link": "Share call link", + "hangup": "Hangup", + "hide_sidebar_button": "Hide sidebar", + "input_devices": "Input devices", + "jitsi_call": "Jitsi Conference", + "join_button_tooltip_call_full": "Sorry — this call is currently full", + "join_button_tooltip_connecting": "Connecting", + "legacy_call": "Legacy Call", + "maximise": "Fill screen", + "maximise_call": "Maximise call", + "metaspace_video_rooms": { + "conference_room_section": "Conferences" + }, + "minimise_call": "Minimise call", + "misconfigured_server": "Call failed due to misconfigured server", + "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "misconfigured_server_fallback": "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "misconfigured_server_fallback_accept": "Try using %(server)s", + "more_button": "More", + "msisdn_lookup_failed": "Unable to look up phone number", + "msisdn_lookup_failed_description": "There was an error looking up the phone number", + "msisdn_transfer_failed": "Unable to transfer call", + "n_people_joined": { + "one": "%(count)s person joined", + "other": "%(count)s people joined" + }, + "no_audio_input_description": "We didn't find a microphone on your device. Please check your settings and try again.", + "no_audio_input_title": "No microphone found", + "no_media_perms_description": "You may need to manually permit %(brand)s to access your microphone/webcam", + "no_media_perms_title": "No media permissions", + "no_permission_conference": "Permission Required", + "no_permission_conference_description": "You do not have permission to start a conference call in this room", + "on_hold": "%(name)s on hold", + "output_devices": "Output devices", + "screenshare_monitor": "Share entire screen", + "screenshare_title": "Share content", + "screenshare_window": "Application window", + "show_sidebar_button": "Show sidebar", + "silence": "Silence call", + "silenced": "Notifications silenced", + "start_screenshare": "Start sharing your screen", + "stop_screenshare": "Stop sharing your screen", + "too_many_calls": "Too Many Calls", + "too_many_calls_description": "You've reached the maximum number of simultaneous calls.", + "transfer_consult_first_label": "Consult first", + "transfer_failed": "Transfer Failed", + "transfer_failed_description": "Failed to transfer call", + "unable_to_access_audio_input_description": "We were unable to access your microphone. Please check your browser settings and try again.", + "unable_to_access_audio_input_title": "Unable to access your microphone", + "unable_to_access_media": "Unable to access webcam / microphone", + "unable_to_access_microphone": "Unable to access microphone", + "unknown_caller": "Unknown caller", + "unknown_person": "unknown person", + "unsilence": "Sound on", + "unsupported": "Calls are unsupported", + "unsupported_browser": "You cannot place calls in this browser.", + "user_busy": "User Busy", + "user_busy_description": "The user you called is busy.", + "user_is_presenting": "%(sharerName)s is presenting", + "video_call": "Video call", + "video_call_started": "Video call started", + "video_call_using": "Video call using:", + "voice_call": "Voice call", + "you_are_presenting": "You are presenting" + }, + "web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s", + "welcome_to_element": "Welcome to Element", + "widget": { + "added_by": "Widget added by", + "capabilities_dialog": { + "content_starting_text": "This widget would like to:", + "decline_all_permission": "Decline All", + "remember_Selection": "Remember my selection for this widget", + "title": "Approve widget permissions" + }, + "capability": { + "always_on_screen_generic": "Remain on your screen while running", + "always_on_screen_viewing_another_room": "Remain on your screen when viewing another room, when running", + "any_room": "The above, but in any room you are joined or invited to as well", + "byline_empty_state_key": "with an empty state key", + "byline_state_key": "with state key %(stateKey)s", + "capability": "The %(capability)s capability", + "change_avatar_active_room": "Change the avatar of your active room", + "change_avatar_this_room": "Change the avatar of this room", + "change_name_active_room": "Change the name of your active room", + "change_name_this_room": "Change the name of this room", + "change_topic_active_room": "Change the topic of your active room", + "change_topic_this_room": "Change the topic of this room", + "receive_membership_active_room": "See when people join, leave, or are invited to your active room", + "receive_membership_this_room": "See when people join, leave, or are invited to this room", + "remove_ban_invite_leave_active_room": "Remove, ban, or invite people to your active room, and make you leave", + "remove_ban_invite_leave_this_room": "Remove, ban, or invite people to this room, and make you leave", + "see_avatar_change_active_room": "See when the avatar changes in your active room", + "see_avatar_change_this_room": "See when the avatar changes in this room", + "see_event_type_sent_active_room": "See %(eventType)s events posted to your active room", + "see_event_type_sent_this_room": "See %(eventType)s events posted to this room", + "see_images_sent_active_room": "See images posted to your active room", + "see_images_sent_this_room": "See images posted to this room", + "see_messages_sent_active_room": "See messages posted to your active room", + "see_messages_sent_this_room": "See messages posted to this room", + "see_msgtype_sent_active_room": "See %(msgtype)s messages posted to your active room", + "see_msgtype_sent_this_room": "See %(msgtype)s messages posted to this room", + "see_name_change_active_room": "See when the name changes in your active room", + "see_name_change_this_room": "See when the name changes in this room", + "see_sent_emotes_active_room": "See emotes posted to your active room", + "see_sent_emotes_this_room": "See emotes posted to this room", + "see_sent_files_active_room": "See general files posted to your active room", + "see_sent_files_this_room": "See general files posted to this room", + "see_sticker_posted_active_room": "See when anyone posts a sticker to your active room", + "see_sticker_posted_this_room": "See when a sticker is posted in this room", + "see_text_messages_sent_active_room": "See text messages posted to your active room", + "see_text_messages_sent_this_room": "See text messages posted to this room", + "see_topic_change_active_room": "See when the topic changes in your active room", + "see_topic_change_this_room": "See when the topic changes in this room", + "see_videos_sent_active_room": "See videos posted to your active room", + "see_videos_sent_this_room": "See videos posted to this room", + "send_emotes_active_room": "Send emotes as you in your active room", + "send_emotes_this_room": "Send emotes as you in this room", + "send_event_type_active_room": "Send %(eventType)s events as you in your active room", + "send_event_type_this_room": "Send %(eventType)s events as you in this room", + "send_files_active_room": "Send general files as you in your active room", + "send_files_this_room": "Send general files as you in this room", + "send_images_active_room": "Send images as you in your active room", + "send_images_this_room": "Send images as you in this room", + "send_messages_active_room": "Send messages as you in your active room", + "send_messages_this_room": "Send messages as you in this room", + "send_msgtype_active_room": "Send %(msgtype)s messages as you in your active room", + "send_msgtype_this_room": "Send %(msgtype)s messages as you in this room", + "send_stickers_active_room": "Send stickers into your active room", + "send_stickers_active_room_as_you": "Send stickers to your active room as you", + "send_stickers_this_room": "Send stickers into this room", + "send_stickers_this_room_as_you": "Send stickers to this room as you", + "send_text_messages_active_room": "Send text messages as you in your active room", + "send_text_messages_this_room": "Send text messages as you in this room", + "send_videos_active_room": "Send videos as you in your active room", + "send_videos_this_room": "Send videos as you in this room", + "specific_room": "The above, but in as well", + "switch_room": "Change which room you're viewing", + "switch_room_message_user": "Change which room, message, or user you're viewing" + }, + "close_to_view_right_panel": "Close this widget to view it in this panel", + "context_menu": { + "delete": "Delete widget", + "delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", + "move_left": "Move left", + "move_right": "Move right", + "remove": "Remove for everyone", + "revoke": "Revoke permissions", + "screenshot": "Take a picture", + "start_audio_stream": "Start audio stream" + }, + "cookie_warning": "This widget may use cookies.", + "error_hangup_description": "You were disconnected from the call. (Error: %(message)s)", + "error_hangup_title": "Connection lost", + "error_loading": "Error loading Widget", + "error_mixed_content": "Error - Mixed content", + "error_need_invite_permission": "You need to be able to invite users to do that.", + "error_need_kick_permission": "You need to be able to kick users to do that.", + "error_need_to_be_logged_in": "You need to be logged in.", + "error_unable_start_audio_stream_description": "Unable to start audio streaming.", + "error_unable_start_audio_stream_title": "Failed to start livestream", + "modal_data_warning": "Data on this screen is shared with %(widgetDomain)s", + "modal_title_default": "Modal Widget", + "no_name": "Unknown App", + "open_id_permissions_dialog": { + "remember_selection": "Remember this", + "starting_text": "The widget will verify your user ID, but won't be able to perform actions for you:", + "title": "Allow this widget to verify your identity" + }, + "popout": "Popout widget", + "set_room_layout": "Set layout for everyone", + "shared_data_avatar": "Your profile picture URL", + "shared_data_device_id": "Your device ID", + "shared_data_lang": "Your language", + "shared_data_mxid": "Your user ID", + "shared_data_name": "Your display name", + "shared_data_room_id": "Room ID", + "shared_data_theme": "Your theme", + "shared_data_url": "%(brand)s URL", + "shared_data_warning": "Using this widget may share data with %(widgetDomain)s.", + "shared_data_warning_im": "Using this widget may share data with %(widgetDomain)s & your integration manager.", + "shared_data_widget_id": "Widget ID", + "unencrypted_warning": "Widgets do not use message encryption.", + "unmaximise": "Un-maximise", + "unpin_to_view_right_panel": "Unpin this widget to view it in this panel" + }, + "zxcvbn": { + "suggestions": { + "allUppercase": "All-uppercase is almost as easy to guess as all-lowercase", + "anotherWord": "Add another word or two. Uncommon words are better.", + "associatedYears": "Avoid years that are associated with you", + "capitalization": "Capitalization doesn't help very much", + "dates": "Avoid dates and years that are associated with you", + "l33t": "Predictable substitutions like '@' instead of 'a' don't help very much", + "longerKeyboardPattern": "Use a longer keyboard pattern with more turns", + "noNeed": "No need for symbols, digits, or uppercase letters", + "pwned": "If you use this password elsewhere, you should change it.", + "recentYears": "Avoid recent years", + "repeated": "Avoid repeated words and characters", + "reverseWords": "Reversed words aren't much harder to guess", + "sequences": "Avoid sequences", + "useWords": "Use a few words, avoid common phrases" + }, + "warnings": { + "common": "This is a very common password", + "commonNames": "Common names and surnames are easy to guess", + "dates": "Dates are often easy to guess", + "extendedRepeat": "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"", + "keyPattern": "Short keyboard patterns are easy to guess", + "namesByThemselves": "Names and surnames by themselves are easy to guess", + "pwned": "Your password was exposed by a data breach on the Internet.", + "recentYears": "Recent years are easy to guess", + "sequences": "Sequences like abc or 6543 are easy to guess", + "similarToCommon": "This is similar to a commonly used password", + "simpleRepeat": "Repeats like \"aaa\" are easy to guess", + "straightRow": "Straight rows of keys are easy to guess", + "topHundred": "This is a top-100 common password", + "topTen": "This is a top-10 common password", + "userInputs": "There should not be any personal or page related data.", + "wordByItself": "A word by itself is easy to guess" } + } } From 5943e41f26d00b6fef9c693855f07d915a1abe04 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 14:43:04 +0000 Subject: [PATCH 36/40] Remove empty constructor --- src/settings/controllers/SlidingSyncController.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 165be14a269..44de3ba8eee 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -14,10 +14,6 @@ import { _t } from "../../languageHandler"; import { SlidingSyncManager } from "../../SlidingSyncManager"; export default class SlidingSyncController extends SettingController { - public constructor() { - super(); - } - public async onChange(): Promise { PlatformPeg.get()?.reload(); } From 4b0c52cf3d66acbf0e8d00c6144b27996137200f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 14:44:20 +0000 Subject: [PATCH 37/40] Remove unrelated changes --- src/components/structures/TimelinePanel.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 195c75366bc..c41240079a6 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -711,6 +711,7 @@ class TimelinePanel extends React.Component { ) { return; } + if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) { if (toStartOfTimeline && !this.state.canBackPaginate) { this.setState({ @@ -1541,6 +1542,7 @@ class TimelinePanel extends React.Component { const onLoaded = (): void => { if (this.unmounted) return; + // clear the timeline min-height when (re)loading the timeline this.messagePanel.current?.onTimelineReset(); this.reloadEvents(); @@ -1642,6 +1644,7 @@ class TimelinePanel extends React.Component { onLoaded(); return; } + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise => { if (this.overlayTimelineWindow) { // TODO: use timestampToEvent to load the overlay timeline From e2e9a463c6ce7d634c7eb1621a2d04dce29f465c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 15:39:50 +0000 Subject: [PATCH 38/40] Unused import --- .../components/viewmodels/roomlist/RoomListViewModel-test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 9babc6cf241..0b7d4e652bd 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -10,7 +10,6 @@ import { act, renderHook, waitFor } from "jest-matrix-react"; import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3"; import { mkStubRoom } from "../../../../test-utils"; -import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/SlidingRoomListStore"; import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; From 862174ae9cc7133ddbbc7b984f13a308f250de9c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 15:44:11 +0000 Subject: [PATCH 39/40] Fix import --- .../components/viewmodels/roomlist/RoomListViewModel-test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 0b7d4e652bd..200bd534d8a 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -14,6 +14,7 @@ import { useRoomListViewModel } from "../../../../../src/components/viewmodels/r import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters"; +import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/RoomListStore"; describe("RoomListViewModel", () => { function mockAndCreateRooms() { From 5f8719fa7dbf34998699f3b1d52067d2245a8855 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Mar 2025 15:44:56 +0000 Subject: [PATCH 40/40] Avoid moving import --- .../components/viewmodels/roomlist/RoomListViewModel-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 200bd534d8a..0f432cef360 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -10,11 +10,11 @@ import { act, renderHook, waitFor } from "jest-matrix-react"; import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3"; import { mkStubRoom } from "../../../../test-utils"; +import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/RoomListStore"; import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters"; -import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/RoomListStore"; describe("RoomListViewModel", () => { function mockAndCreateRooms() {