diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b8e22f013..b82d5bc48e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) +===================================================================================================== + +## ✨ Features + * Device manager - scroll to filtered list from security recommendations ([\#9227](https://github.com/matrix-org/matrix-react-sdk/pull/9227)). Contributed by @kerryarchibald. + * Device manager - updated dropdown style in filtered device list ([\#9226](https://github.com/matrix-org/matrix-react-sdk/pull/9226)). Contributed by @kerryarchibald. + * Device manager - device type and verification icons on device tile ([\#9197](https://github.com/matrix-org/matrix-react-sdk/pull/9197)). Contributed by @kerryarchibald. + * Ignore unreads in low priority rooms in the space panel ([\#6518](https://github.com/matrix-org/matrix-react-sdk/pull/6518)). Fixes vector-im/element-web#16836. + * Release message right-click context menu out of labs ([\#8613](https://github.com/matrix-org/matrix-react-sdk/pull/8613)). + * Device manager - expandable session details in device list ([\#9188](https://github.com/matrix-org/matrix-react-sdk/pull/9188)). Contributed by @kerryarchibald. + * Device manager - device list filtering ([\#9181](https://github.com/matrix-org/matrix-react-sdk/pull/9181)). Contributed by @kerryarchibald. + * Device manager - add verification details to session details ([\#9187](https://github.com/matrix-org/matrix-react-sdk/pull/9187)). Contributed by @kerryarchibald. + * Device manager - current session expandable details ([\#9185](https://github.com/matrix-org/matrix-react-sdk/pull/9185)). Contributed by @kerryarchibald. + * Device manager - security recommendations section ([\#9179](https://github.com/matrix-org/matrix-react-sdk/pull/9179)). Contributed by @kerryarchibald. + * The Welcome Home Screen: Return Button ([\#9089](https://github.com/matrix-org/matrix-react-sdk/pull/9089)). Fixes vector-im/element-web#22917. Contributed by @justjanne. + * Device manager - label devices as inactive ([\#9175](https://github.com/matrix-org/matrix-react-sdk/pull/9175)). Contributed by @kerryarchibald. + * Device manager - other sessions list ([\#9155](https://github.com/matrix-org/matrix-react-sdk/pull/9155)). Contributed by @kerryarchibald. + * Implement MSC3846: Allowing widgets to access TURN servers ([\#9061](https://github.com/matrix-org/matrix-react-sdk/pull/9061)). + * Allow widgets to send/receive to-device messages ([\#8885](https://github.com/matrix-org/matrix-react-sdk/pull/8885)). + +## 🐛 Bug Fixes + * Add super cool feature ([\#9222](https://github.com/matrix-org/matrix-react-sdk/pull/9222)). Contributed by @gefgu. + * Make use of js-sdk roomNameGenerator to handle i18n for generated room names ([\#9209](https://github.com/matrix-org/matrix-react-sdk/pull/9209)). Fixes vector-im/element-web#21369. + * Fix progress bar regression throughout the app ([\#9219](https://github.com/matrix-org/matrix-react-sdk/pull/9219)). Fixes vector-im/element-web#23121. + * Reuse empty string & space string logic for event types in devtools ([\#9218](https://github.com/matrix-org/matrix-react-sdk/pull/9218)). Fixes vector-im/element-web#23115. + * Reduce amount of requests done by the onboarding task list ([\#9194](https://github.com/matrix-org/matrix-react-sdk/pull/9194)). Fixes vector-im/element-web#23085. Contributed by @justjanne. + * Avoid hardcoding branding in user onboarding ([\#9206](https://github.com/matrix-org/matrix-react-sdk/pull/9206)). Fixes vector-im/element-web#23111. Contributed by @justjanne. + * End jitsi call when member is banned ([\#8879](https://github.com/matrix-org/matrix-react-sdk/pull/8879)). Contributed by @maheichyk. + * Fix context menu being opened when clicking message action bar buttons ([\#9200](https://github.com/matrix-org/matrix-react-sdk/pull/9200)). Fixes vector-im/element-web#22279 and vector-im/element-web#23100. + * Add gap between checkbox and text in report dialog following the same pattern (8px) used in the gap between the two buttons. It fixes vector-im/element-web#23060 ([\#9195](https://github.com/matrix-org/matrix-react-sdk/pull/9195)). Contributed by @gefgu. + * Fix url preview AXE and layout issue & add percy test ([\#9189](https://github.com/matrix-org/matrix-react-sdk/pull/9189)). Fixes vector-im/element-web#23083. + * Wrap long space names ([\#9201](https://github.com/matrix-org/matrix-react-sdk/pull/9201)). Fixes vector-im/element-web#23095. + * Attempt to fix `Failed to execute 'removeChild' on 'Node'` ([\#9196](https://github.com/matrix-org/matrix-react-sdk/pull/9196)). + * Fix soft crash around space hierarchy changing between spaces ([\#9191](https://github.com/matrix-org/matrix-react-sdk/pull/9191)). Fixes matrix-org/element-web-rageshakes#14613. + * Fix soft crash around room view store metrics ([\#9190](https://github.com/matrix-org/matrix-react-sdk/pull/9190)). Fixes matrix-org/element-web-rageshakes#14361. + * Fix the same person appearing multiple times when searching for them. ([\#9177](https://github.com/matrix-org/matrix-react-sdk/pull/9177)). Fixes vector-im/element-web#22851. + * Fix space panel subspace indentation going missing ([\#9167](https://github.com/matrix-org/matrix-react-sdk/pull/9167)). Fixes vector-im/element-web#23049. + * Fix invisible power levels tile when showing hidden events ([\#9162](https://github.com/matrix-org/matrix-react-sdk/pull/9162)). Fixes vector-im/element-web#23013. + * Space panel accessibility improvements ([\#9157](https://github.com/matrix-org/matrix-react-sdk/pull/9157)). Fixes vector-im/element-web#22995. + * Fix inverted logic for showing UserWelcomeTop component ([\#9164](https://github.com/matrix-org/matrix-react-sdk/pull/9164)). Fixes vector-im/element-web#23037. + Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.52.0) (2022-08-16) ===================================================================================================== diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts index 0a8212ab8dd..e7767de9421 100644 --- a/cypress/e2e/spaces/spaces.spec.ts +++ b/cypress/e2e/spaces/spaces.spec.ts @@ -237,4 +237,42 @@ describe("Spaces", () => { cy.contains(".mx_SpaceHierarchy_roomTile", "Gaming").should("exist"); }); }); + + it("should render subspaces in the space panel only when expanded", () => { + cy.injectAxe(); + + cy.createSpace({ + name: "Child Space", + initial_state: [], + }).then(spaceId => { + cy.createSpace({ + name: "Root Space", + initial_state: [ + spaceChildInitialState(spaceId), + ], + }).as("spaceId"); + }); + cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist"); + cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Child Space"]').should("not.exist"); + + const axeOptions = { + rules: { + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + 'nested-interactive': { + enabled: false, + }, + }, + }; + cy.checkA11y(undefined, axeOptions); + cy.get(".mx_SpacePanel").percySnapshotElement("Space panel collapsed", { widths: [68] }); + + cy.get(".mx_SpaceButton_toggleCollapse").click({ force: true }); + cy.get(".mx_SpacePanel:not(.collapsed)").should("exist"); + + cy.contains(".mx_SpaceItem", "Root Space").should("exist") + .contains(".mx_SpaceItem", "Child Space").should("exist"); + + cy.checkA11y(undefined, axeOptions); + cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] }); + }); }); diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index fee1e390713..d4b2d2cf9b0 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -114,6 +114,7 @@ Cypress.Commands.add("startDM", (name: string) => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(name); + cy.wait(1000); // wait for the dialog code to settle cy.get(".mx_Spinner").should("not.exist"); cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", name); @@ -216,6 +217,7 @@ describe("Spotlight", () => { it("should find joined rooms", () => { cy.openSpotlightDialog().within(() => { cy.spotlightSearch().clear().type(room1Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().eq(0).click(); @@ -229,6 +231,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room1Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().eq(0).should("contain", "View"); @@ -243,6 +246,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room2Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room2Name); cy.spotlightResults().eq(0).should("contain", "Join"); @@ -258,6 +262,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room3Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room3Name); cy.spotlightResults().eq(0).should("contain", "View"); @@ -296,6 +301,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot1Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", bot1Name); cy.spotlightResults().eq(0).click(); @@ -308,6 +314,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().eq(0).click(); @@ -324,6 +331,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().eq(0).click(); @@ -341,27 +349,53 @@ describe("Spotlight", () => { cy.get(".mx_RoomSublist[aria-label=People]").should("contain", bot2Name); // Invite BotBob into existing DM with ByteBot - cy.getDmRooms(bot2.getUserId()).then(dmRooms => dmRooms[0]) - .then(groupDmId => cy.inviteUser(groupDmId, bot1.getUserId())) - .then(() => { - cy.roomHeaderName().should("contain", `${bot1Name} and ${bot2Name}`); - cy.get(".mx_RoomSublist[aria-label=People]").should("contain", `${bot1Name} and ${bot2Name}`); + cy.getDmRooms(bot2.getUserId()) + .should("have.length", 1) + .then(dmRooms => cy.getClient().then(client => client.getRoom(dmRooms[0]))) + .then(groupDm => { + cy.inviteUser(groupDm.roomId, bot1.getUserId()); + cy.roomHeaderName().should(($element) => + expect($element.get(0).innerText).contains(groupDm.name)); + cy.get(".mx_RoomSublist[aria-label=People]").should(($element) => + expect($element.get(0).innerText).contains(groupDm.name)); + + // Search for BotBob by id, should return group DM and user + cy.openSpotlightDialog().within(() => { + cy.spotlightFilter(Filter.People); + cy.spotlightSearch().clear().type(bot1.getUserId()); + cy.wait(1000); // wait for the dialog code to settle + cy.spotlightResults().should("have.length", 2); + cy.spotlightResults().eq(0).should("contain", groupDm.name); + }); + + // Search for ByteBot by id, should return group DM and user + cy.openSpotlightDialog().within(() => { + cy.spotlightFilter(Filter.People); + cy.spotlightSearch().clear().type(bot2.getUserId()); + cy.wait(1000); // wait for the dialog code to settle + cy.spotlightResults().should("have.length", 2); + cy.spotlightResults().eq(0).should("contain", groupDm.name); + }); }); + }); - // Search for BotBob by id, should return group DM and user + // Test against https://github.com/vector-im/element-web/issues/22851 + it("should show each person result only once", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot1.getUserId()); - cy.spotlightResults().should("have.length", 2); - cy.spotlightResults().eq(0).should("contain", `${bot1Name} and ${bot2Name}`); - }); - // Search for ByteBot by id, should return group DM and user - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.People); - cy.spotlightSearch().clear().type(bot2.getUserId()); - cy.spotlightResults().should("have.length", 2); - cy.spotlightResults().eq(0).should("contain", `${bot1Name} and ${bot2Name}`); + // 2 rounds of search to simulate the bug conditions. Specifically, the first search + // should have 1 result (not 2) and the second search should also have 1 result (instead + // of the super buggy 3 described by https://github.com/vector-im/element-web/issues/22851) + // + // We search for user ID to trigger the profile lookup within the dialog. + for (let i = 0; i < 2; i++) { + cy.log("Iteration: " + i); + cy.spotlightSearch().clear().type(bot1.getUserId()); + cy.wait(1000); // wait for the dialog code to settle + cy.spotlightResults().should("have.length", 1); + cy.spotlightResults().eq(0).should("contain", bot1.getUserId()); + } }); }); @@ -369,6 +403,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); + cy.wait(1000); // wait for the dialog code to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", bot2Name); cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat"); @@ -390,6 +425,7 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot1Name); + cy.wait(1000); // wait for the dialog code to settle cy.get(".mx_Spinner").should("not.exist"); cy.spotlightResults().should("have.length", 1); }); diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index 6eacacfed23..94b6ffaa425 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -155,7 +155,7 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); cy.get(".mx_Spinner").should("not.exist"); cy.percySnapshot("Configured room on IRC layout"); }); @@ -166,7 +166,7 @@ describe("Timeline", () => { // Wait until configuration is finished cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Click "expand" link button cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); @@ -193,14 +193,14 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary", - "created and configured the room."); + "created and configured the room.").should("exist"); // Edit message cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}"); }); - cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "MessageEdit"); + cy.contains(".mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); // Click timestamp to highlight hidden event line cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); @@ -228,18 +228,19 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Edit message cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}"); }); - cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit"); + cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area - cy.get(".mx_EventTile .mx_ViewSourceEvent").realHover() - .get(".mx_EventTile .mx_ViewSourceEvent .mx_ViewSourceEvent_toggle").click('topLeft', { force: false }); + cy.get(".mx_EventTile .mx_ViewSourceEvent").should("exist").realHover().within(() => { + cy.get(".mx_ViewSourceEvent_toggle").click('topLeft', { force: false }); + }); // Make sure the expand toggle worked cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible"); @@ -249,17 +250,17 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Click "expand" link button cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); // Click "collapse" link button on the first hovered info event line - cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type").realHover() - .get(".mx_GenericEventListSummary_toggle[aria-expanded=true]").click({ force: false }); + cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type").realHover(); + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=true]").click({ force: false }); // Make sure "collapse" link button worked - cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]"); + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").should("exist"); }); it("should highlight search result words regardless of formatting", () => { @@ -273,6 +274,49 @@ describe("Timeline", () => { cy.get(".mx_EventTile:not(.mx_EventTile_contextual)").find(".mx_EventTile_searchHighlight").should("exist"); cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Highlighted search results"); }); + + it("should render url previews", () => { + cy.intercept("**/_matrix/media/r0/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", { + statusCode: 200, + fixture: "riot.png", + headers: { + "Content-Type": "image/png", + }, + }).as("mxc"); + cy.intercept("**/_matrix/media/r0/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", { + statusCode: 200, + body: { + "og:title": "Element Call", + "og:description": null, + "og:image:width": 48, + "og:image:height": 48, + "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", + "og:image:type": "image/png", + "matrix:image:size": 2121, + }, + headers: { + "Content-Type": "application/json", + }, + }).as("preview_url"); + + cy.sendEvent( + roomId, + null, + "m.room.message" as EventType, + MessageEvent.from("https://call.element.io/").serialize().content, + ); + cy.visit("/#/room/" + roomId); + + cy.get(".mx_LinkPreviewWidget").should("exist").should("contain.text", "Element Call"); + + cy.wait("@preview_url"); + cy.wait("@mxc"); + + cy.checkA11y(); + cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", { + widths: [800, 400], + }); + }); }); describe("message sending", () => { @@ -285,7 +329,7 @@ describe("Timeline", () => { cy.getComposer().type(`${MESSAGE}{enter}`); // Reply to the message - cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile_line", "Hello world").within(() => { + cy.get(".mx_RoomView_body").contains(".mx_EventTile_line", "Hello world").within(() => { cy.get('[aria-label="Reply"]').click({ force: true }); // Cypress has no ability to hover }); }; @@ -296,20 +340,22 @@ describe("Timeline", () => { cy.getComposer().type(`${reply}{enter}`); - cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody") + cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody") .should("contain", MESSAGE); - cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody").contains(reply) + cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply) .should("have.length", 1); }); - xit("can reply with a voice message", () => { + it("can reply with a voice message", () => { viewRoomSendMessageAndSetupReply(); - cy.openMessageComposerOptions().find(`[aria-label="Voice Message"]`).click(); + cy.openMessageComposerOptions().within(() => { + cy.get(`[aria-label="Voice Message"]`).click(); + }); cy.wait(3000); - cy.getComposer().find(".mx_MessageComposer_sendMessage").click(); + cy.get(".mx_RoomView_body .mx_MessageComposer .mx_MessageComposer_sendMessage").click(); - cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody") + cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody") .should("contain", MESSAGE); cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody") .should("have.length", 1); diff --git a/cypress/e2e/user-onboarding/user-onboarding-new.ts b/cypress/e2e/user-onboarding/user-onboarding-new.ts index 44787ee61e8..c6eac8ce27d 100644 --- a/cypress/e2e/user-onboarding/user-onboarding-new.ts +++ b/cypress/e2e/user-onboarding/user-onboarding-new.ts @@ -40,6 +40,13 @@ describe("User Onboarding (new user)", () => { bot1 = _bot1; }); cy.get('.mx_UserOnboardingPage').should('exist'); + cy.get('.mx_UserOnboardingButton').should('exist'); + cy.get('.mx_UserOnboardingList') + .should('exist') + .should(($list) => { + const list = $list.get(0); + expect(getComputedStyle(list).opacity).to.be.eq("1"); + }); }); }); @@ -47,20 +54,14 @@ describe("User Onboarding (new user)", () => { cy.stopSynapse(synapse); }); - it("page is shown", () => { - cy.get('.mx_UserOnboardingPage').should('exist'); - cy.get('.mx_UserOnboardingList') - .should('exist') - .should(($list) => { - const list = $list.get(0); - expect(getComputedStyle(list).opacity).to.be.eq("1"); - }); + it("page is shown and preference exists", () => { cy.get('.mx_UserOnboardingPage') .percySnapshotElement("User onboarding page"); + cy.openUserSettings("Preferences"); + cy.contains("Show shortcut to welcome checklist above the room list").should("exist"); }); it("app download dialog", () => { - cy.get('.mx_UserOnboardingPage').should('exist'); cy.contains(".mx_UserOnboardingTask_action", "Download apps").click(); cy.get('[role=dialog]') .contains("#mx_BaseDialog_title", "Download Element") @@ -79,8 +80,18 @@ describe("User Onboarding (new user)", () => { cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId()); cy.get(".mx_InviteDialog_buttonAndSpinner").click(); cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist"); + const message = "Hi!"; + cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`); + cy.contains(".mx_MTextBody.mx_EventTile_content", message); cy.visit("/#/home"); - + cy.get('.mx_UserOnboardingPage').should('exist'); + cy.get('.mx_UserOnboardingButton').should('exist'); + cy.get('.mx_UserOnboardingList') + .should('exist') + .should(($list) => { + const list = $list.get(0); + expect(getComputedStyle(list).opacity).to.be.eq("1"); + }); cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress); }); }); diff --git a/cypress/e2e/user-onboarding/user-onboarding-old.ts b/cypress/e2e/user-onboarding/user-onboarding-old.ts index 2be066e0a1c..f079ed9a4c3 100644 --- a/cypress/e2e/user-onboarding/user-onboarding-old.ts +++ b/cypress/e2e/user-onboarding/user-onboarding-old.ts @@ -40,7 +40,10 @@ describe("User Onboarding (old user)", () => { cy.stopSynapse(synapse); }); - it("page is hidden", () => { + it("page and preference are hidden", () => { cy.get('.mx_UserOnboardingPage').should('not.exist'); + cy.get('.mx_UserOnboardingButton').should('not.exist'); + cy.openUserSettings("Preferences"); + cy.contains("Show shortcut to welcome page above the room list").should("not.exist"); }); }); diff --git a/cypress/e2e/widgets/widget-pip-close.spec.ts b/cypress/e2e/widgets/widget-pip-close.spec.ts new file mode 100644 index 00000000000..7689e38ed00 --- /dev/null +++ b/cypress/e2e/widgets/widget-pip-close.spec.ts @@ -0,0 +1,201 @@ +/* +Copyright 2022 Mikhail Aheichyk +Copyright 2022 Nordeck IT + Consulting GmbH. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; + +import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { SynapseInstance } from "../../plugins/synapsedocker"; +import { UserCredentials } from "../../support/login"; + +const DEMO_WIDGET_ID = "demo-widget-id"; +const DEMO_WIDGET_NAME = "Demo Widget"; +const DEMO_WIDGET_TYPE = "demo"; +const ROOM_NAME = "Demo"; + +const DEMO_WIDGET_HTML = ` + + + Demo Widget + + + + + + +`; + +// mostly copied from src/utils/WidgetUtils.waitForRoomWidget with small modifications +function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: string, add: boolean): Promise { + const matrixClient = win.mxMatrixClientPeg.get(); + + return new Promise((resolve, reject) => { + function eventsInIntendedState(evList) { + const widgetPresent = evList.some((ev) => { + return ev.getContent() && ev.getContent()['id'] === widgetId; + }); + if (add) { + return widgetPresent; + } else { + return !widgetPresent; + } + } + + const room = matrixClient.getRoom(roomId); + + const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + if (eventsInIntendedState(startingWidgetEvents)) { + resolve(); + return; + } + + function onRoomStateEvents(ev: MatrixEvent) { + if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return; + + const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + + if (eventsInIntendedState(currentWidgetEvents)) { + matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents); + resolve(); + } + } + + matrixClient.on(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents); + }); +} + +describe("Widget PIP", () => { + let synapse: SynapseInstance; + let user: UserCredentials; + let bot: MatrixClient; + let demoWidgetUrl: string; + + function roomCreateAddWidgetPip(userRemove: 'leave' | 'kick' | 'ban') { + cy.createRoom({ + name: ROOM_NAME, + invite: [bot.getUserId()], + }).then(roomId => { + // sets bot to Admin and user to Moderator + cy.getClient().then(matrixClient => { + return matrixClient.sendStateEvent(roomId, 'm.room.power_levels', { + users: { + [user.userId]: 50, + [bot.getUserId()]: 100, + }, + }); + }).as('powerLevelsChanged'); + + // bot joins the room + cy.botJoinRoom(bot, roomId).as('botJoined'); + + // setup widget via state event + cy.getClient().then(async matrixClient => { + const content: IWidget = { + id: DEMO_WIDGET_ID, + creatorUserId: 'somebody', + type: DEMO_WIDGET_TYPE, + name: DEMO_WIDGET_NAME, + url: demoWidgetUrl, + }; + await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID); + }).as('widgetEventSent'); + + // open the room + cy.viewRoomByName(ROOM_NAME); + + cy.all([ + cy.get("@powerLevelsChanged"), + cy.get("@botJoined"), + cy.get("@widgetEventSent"), + ]).then(() => { + cy.window().then(async win => { + // wait for widget state event + await waitForRoomWidget(win, DEMO_WIDGET_ID, roomId, true); + + // activate widget in pip mode + win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true); + + // checks that pip window is opened + cy.get(".mx_CallView_pip").should("exist"); + + // checks that widget is opened in pip + cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => { + cy.get("#demo").should('exist').then(async () => { + const userId = user.userId; + if (userRemove == 'leave') { + cy.getClient().then(async matrixClient => { + await matrixClient.leave(roomId); + }); + } else if (userRemove == 'kick') { + await bot.kick(roomId, userId); + } else if (userRemove == 'ban') { + await bot.ban(roomId, userId); + } + + // checks that pip window is closed + cy.get(".mx_CallView_pip").should("not.exist"); + }); + }); + }); + }); + }); + } + + beforeEach(() => { + cy.startSynapse("default").then(data => { + synapse = data; + + cy.initTestUser(synapse, "Mike").then(_user => { + user = _user; + }); + cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then(_bot => { + bot = _bot; + }); + }); + cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => { + demoWidgetUrl = url; + }); + }); + + afterEach(() => { + cy.stopSynapse(synapse); + cy.stopWebServers(); + }); + + it('should be closed on leave', () => { + roomCreateAddWidgetPip('leave'); + }); + + it('should be closed on kick', () => { + roomCreateAddWidgetPip('kick'); + }); + + it('should be closed on ban', () => { + roomCreateAddWidgetPip('ban'); + }); +}); diff --git a/cypress/plugins/synapsedocker/templates/consent/log.config b/cypress/plugins/synapsedocker/templates/consent/log.config index ac232762da3..b9123d0f5b9 100644 --- a/cypress/plugins/synapsedocker/templates/consent/log.config +++ b/cypress/plugins/synapsedocker/templates/consent/log.config @@ -26,7 +26,7 @@ loggers: synapse.storage.SQL: # beware: increasing this to DEBUG will make synapse log sensitive # information such as access tokens. - level: INFO + level: DEBUG twisted: # We send the twisted logging directly to the file handler, @@ -36,7 +36,7 @@ loggers: propagate: false root: - level: INFO + level: DEBUG # Write logs to the `buffer` handler, which will buffer them together in memory, # then write them to a file. diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index 842009bcae4..347dadc88f4 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -22,8 +22,29 @@ log_config: "/data/log.config" rc_messages_per_second: 10000 rc_message_burst_count: 10000 rc_registration: - per_second: 10000 - burst_count: 10000 + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 rc_login: address: diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/cypress/plugins/synapsedocker/templates/default/log.config index ac232762da3..b9123d0f5b9 100644 --- a/cypress/plugins/synapsedocker/templates/default/log.config +++ b/cypress/plugins/synapsedocker/templates/default/log.config @@ -26,7 +26,7 @@ loggers: synapse.storage.SQL: # beware: increasing this to DEBUG will make synapse log sensitive # information such as access tokens. - level: INFO + level: DEBUG twisted: # We send the twisted logging directly to the file handler, @@ -36,7 +36,7 @@ loggers: propagate: false root: - level: INFO + level: DEBUG # Write logs to the `buffer` handler, which will buffer them together in memory, # then write them to a file. diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts index 06ec815364b..ec07df93aa1 100644 --- a/cypress/support/settings.ts +++ b/cypress/support/settings.ts @@ -82,7 +82,7 @@ declare global { * @param {*} value The new value of the setting, may be null. * @return {Promise} Resolves when the setting has been changed. */ - setSettingValue(name: string, roomId: string, level: SettingLevel, value: any): Chainable; + setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Chainable; /** * Gets the value of a setting. The room ID is optional if the @@ -96,7 +96,7 @@ declare global { * value. * @return {*} The value, or null if not found */ - getSettingValue(name: string, roomId?: string, excludeDefault?: boolean): Chainable; + getSettingValue(settingName: string, roomId?: string, excludeDefault?: boolean): Chainable; } } } diff --git a/package.json b/package.json index 74348fd255c..5bec3e48da5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.52.0", + "version": "3.53.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -93,8 +93,8 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "19.3.0", - "matrix-widget-api": "^0.1.0-beta.18", + "matrix-js-sdk": "19.4.0", + "matrix-widget-api": "^1.0.0", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9440de2c26b..d5274861f17 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -27,7 +27,12 @@ @import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; +@import "./components/views/settings/devices/_DeviceDetails.pcss"; +@import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss"; +@import "./components/views/settings/devices/_DeviceSecurityCard.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; +@import "./components/views/settings/devices/_FilteredDeviceList.pcss"; +@import "./components/views/settings/devices/_SecurityRecommendations.pcss"; @import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @@ -329,6 +334,7 @@ @import "./views/toasts/_IncomingCallToast.pcss"; @import "./views/toasts/_NonUrgentEchoFailureToast.pcss"; @import "./views/typography/_Heading.pcss"; +@import "./views/user-onboarding/_UserOnboardingButton.pcss"; @import "./views/user-onboarding/_UserOnboardingFeedback.pcss"; @import "./views/user-onboarding/_UserOnboardingHeader.pcss"; @import "./views/user-onboarding/_UserOnboardingList.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss new file mode 100644 index 00000000000..3017935bb7b --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss @@ -0,0 +1,74 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_DeviceDetails { + display: flex; + flex-direction: column; + box-sizing: border-box; + + width: 100%; + + margin-top: $spacing-16; + padding: $spacing-16; + border-radius: 8px; + border: 1px solid $quinary-content; +} + +.mx_DeviceDetails_section { + padding-bottom: $spacing-16; + margin-bottom: $spacing-16; + border-bottom: 1px solid $quinary-content; + + display: grid; + grid-gap: $spacing-16; + + &:last-child { + padding-bottom: 0; + border-bottom: 0; + margin-bottom: 0; + } +} + +.mx_DeviceDetails_sectionHeading { + margin: 0; +} + +.mxDeviceDetails_metadataTable { + font-size: $font-12px; + color: $secondary-content; + + width: 100%; + + border-spacing: 0; + + th { + text-transform: uppercase; + font-weight: normal; + text-align: left; + } + + td { + padding-top: $spacing-8; + } + + .mxDeviceDetails_metadataLabel { + width: 160px; + } + + .mxDeviceDetails_metadataValue { + color: $primary-content; + } +} diff --git a/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss new file mode 100644 index 00000000000..4c9d787fdbe --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_DeviceExpandDetailsButton { + height: 32px; + width: 32px; + background: transparent; + + border-radius: 4px; + color: $secondary-content; + + --icon-transform: rotate(-90deg); +} + +.mx_DeviceExpandDetailsButton.mx_DeviceExpandDetailsButton_expanded { + --icon-transform: rotate(0deg); + + background: $system; +} + +.mx_DeviceExpandDetailsButton_icon { + height: 12px; + width: 12px; + + transition: all 0.3s; + transform: var(--icon-transform); + transform-origin: center; +} diff --git a/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss new file mode 100644 index 00000000000..2c267b43144 --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss @@ -0,0 +1,70 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_DeviceSecurityCard { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + box-sizing: border-box; + + padding: $spacing-16; + + border: 1px solid $quinary-content; + border-radius: 8px; +} + +.mx_DeviceSecurityCard_icon { + flex: 0 0 40px; + display: flex; + align-items: center; + justify-content: center; + margin-right: $spacing-16; + border-radius: 8px; + + height: 40px; + width: 40px; + + color: var(--icon-color); + background-color: var(--background-color); + + &.Verified { + --icon-color: $e2e-verified-color; + --background-color: $e2e-verified-color-light; + } + + &.Unverified { + --icon-color: $e2e-warning-color; + --background-color: $e2e-warning-color-light; + } + + &.Inactive { + --icon-color: $secondary-content; + --background-color: $system; + } +} + +.mx_DeviceSecurityCard_content { + flex: 1 1; +} +.mx_DeviceSecurityCard_heading { + margin: 0 0 $spacing-4 0; +} +.mx_DeviceSecurityCard_description { + margin: 0; + font-size: $font-12px; + color: $secondary-content; +} diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss index 159cace6ac0..d89fd9c76eb 100644 --- a/res/css/components/views/settings/devices/_DeviceTile.pcss +++ b/res/css/components/views/settings/devices/_DeviceTile.pcss @@ -18,7 +18,6 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; - width: 100%; } @@ -27,15 +26,21 @@ limitations under the License. } .mx_DeviceTile_metadata { - margin-top: 2px; + margin-top: $spacing-4; font-size: $font-12px; color: $secondary-content; + line-height: $font-14px; +} + +.mx_DeviceTile_inactiveIcon { + height: 14px; + margin-right: $spacing-8; + vertical-align: middle; } .mx_DeviceTile_actions { display: grid; grid-gap: $spacing-8; grid-auto-flow: column; - margin-left: $spacing-8; } diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss new file mode 100644 index 00000000000..01c8df787ef --- /dev/null +++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss @@ -0,0 +1,64 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_FilteredDeviceList { + .mx_Dropdown { + flex: 1 0 80px; + } +} + +.mx_FilteredDeviceList_header { + display: flex; + flex-direction: row; + align-items: center; + box-sizing: border-box; + + width: 100%; + height: 48px; + padding: 0 $spacing-16; + margin-bottom: $spacing-32; + + background-color: $system; + border-radius: 8px; + color: $secondary-content; +} + +.mx_FilteredDeviceList_headerLabel { + flex: 1 1 100%; +} + +.mx_FilteredDeviceList_list { + list-style-type: none; + display: grid; + grid-gap: $spacing-16; + margin: 0; + padding: 0 $spacing-8; +} + +.mx_FilteredDeviceList_listItem { + display: flex; + flex-direction: column; +} + +.mx_FilteredDeviceList_securityCard { + margin-bottom: $spacing-32; +} + +.mx_FilteredDeviceList_noResults { + width: 100%; + text-align: center; + margin-bottom: $spacing-32; +} diff --git a/res/css/components/views/settings/devices/_SecurityRecommendations.pcss b/res/css/components/views/settings/devices/_SecurityRecommendations.pcss new file mode 100644 index 00000000000..d0a53335590 --- /dev/null +++ b/res/css/components/views/settings/devices/_SecurityRecommendations.pcss @@ -0,0 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SecurityRecommendations_spacing { + height: $spacing-16; +} diff --git a/res/css/structures/_HomePage.pcss b/res/css/structures/_HomePage.pcss index 3edf4b7f6be..a3cc194548f 100644 --- a/res/css/structures/_HomePage.pcss +++ b/res/css/structures/_HomePage.pcss @@ -37,15 +37,15 @@ limitations under the License. } h1 { - font-weight: 600; + font-weight: $font-semi-bold; font-size: $font-32px; line-height: $font-44px; margin-bottom: 4px; } - h4 { + h2 { margin-top: 4px; - font-weight: 600; + font-weight: $font-semi-bold; font-size: $font-18px; line-height: $font-25px; color: $muted-fg-color; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 6495d366923..f142a45a990 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -78,10 +78,6 @@ $activeBorderColor: $primary-content; margin: 0; list-style: none; padding: 0; - - > .mx_SpaceItem { - padding-left: 16px; - } } .mx_SpaceButton_toggleCollapse { @@ -294,8 +290,8 @@ $activeBorderColor: $primary-content; } } - // SC: doubled .mx_SpaceTreeLevel: Size down only space avatars of second hierarchy and down .mx_SpaceTreeLevel { + // SC: doubled .mx_SpaceTreeLevel: Size down only space avatars of second hierarchy and down .mx_SpaceButton { .mx_BaseAvatar_image, .mx_BaseAvatar_initial { border-radius: 8px; @@ -304,6 +300,9 @@ $activeBorderColor: $primary-content; line-height: $nestedHeight !important; } } + + // Indent subspaces + padding-left: 16px; } } @@ -393,11 +392,16 @@ $activeBorderColor: $primary-content; } .mx_SpacePanel_contextMenu { + max-width: 360px; + .mx_SpacePanel_contextMenu_header { margin: 12px 16px 12px; font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .mx_SpacePanel_iconHome::before { diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index b39f57cbb01..2664549b170 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -177,6 +177,10 @@ $SpaceRoomViewInnerWidth: 428px; h1 { display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; } } diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 548ee9004f3..5e360203ab3 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -76,6 +76,12 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/x.svg'); } } + + &.mx_AccessibleButton_kind_icon { + padding: 0; + height: 32px; + width: 32px; + } } &.mx_AccessibleButton_kind_primary, diff --git a/res/css/views/elements/_LabelledCheckbox.pcss b/res/css/views/elements/_LabelledCheckbox.pcss index d280d27ebae..8545c6747b4 100644 --- a/res/css/views/elements/_LabelledCheckbox.pcss +++ b/res/css/views/elements/_LabelledCheckbox.pcss @@ -16,6 +16,7 @@ limitations under the License. .mx_LabelledCheckbox { display: flex; + gap: 8px; flex-direction: row; .mx_Checkbox { diff --git a/res/css/views/messages/_DisambiguatedProfile.pcss b/res/css/views/messages/_DisambiguatedProfile.pcss index ef4bc7cfb30..4863dc3518c 100644 --- a/res/css/views/messages/_DisambiguatedProfile.pcss +++ b/res/css/views/messages/_DisambiguatedProfile.pcss @@ -34,3 +34,10 @@ limitations under the License. color: $primary-content; } } + +@media only percy { + .mx_DisambiguatedProfile_displayName { + /* Override the colour in percy tests for screenshot consistency */ + color: $username-variant1-color !important; + } +} diff --git a/res/css/views/rooms/_LinkPreviewWidget.pcss b/res/css/views/rooms/_LinkPreviewWidget.pcss index e42e3a124b7..3c8991df6e7 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.pcss +++ b/res/css/views/rooms/_LinkPreviewWidget.pcss @@ -35,6 +35,7 @@ limitations under the License. display: flex; flex-wrap: wrap; row-gap: $spacing-8; + flex: 1; .mx_LinkPreviewWidget_image, .mx_LinkPreviewWidget_caption { diff --git a/res/css/views/user-onboarding/_UserOnboardingButton.pcss b/res/css/views/user-onboarding/_UserOnboardingButton.pcss new file mode 100644 index 00000000000..3eba86045ac --- /dev/null +++ b/res/css/views/user-onboarding/_UserOnboardingButton.pcss @@ -0,0 +1,84 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_UserOnboardingButton { + display: flex; + flex-direction: column; + align-content: stretch; + align-items: stretch; + border-radius: 8px; + margin: $spacing-8 $spacing-8 0; + padding: $spacing-12; + + &.mx_UserOnboardingButton_selected, + &:hover, + &:focus-within { + background-color: $panel-actions; + } + + .mx_UserOnboardingButton_content { + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; + + .mx_Heading_h4 { + margin-right: auto; + font-size: $font-14px; + color: $primary-content; + } + + .mx_UserOnboardingButton_percentage { + font-size: $font-12px; + color: $secondary-content; + } + + .mx_UserOnboardingButton_close { + position: relative; + box-sizing: border-box; + width: 14px; + height: 14px; + border-radius: 7px; + border: 1px solid $secondary-content; + flex-shrink: 0; + + &::before { + background-color: $secondary-content; + content: ""; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + width: 7px; + height: 7px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + mask-image: url("$(res)/img/element-icons/cancel-rounded.svg"); + } + } + } + + .mx_ProgressBar { + width: auto; + margin-top: $spacing-8; + background: $background; + } + + &.mx_UserOnboardingButton_completed .mx_ProgressBar { + display: none; + } +} diff --git a/res/img/e2e/verified-deprecated.svg b/res/img/e2e/verified-deprecated.svg new file mode 100644 index 00000000000..f90d9db554c --- /dev/null +++ b/res/img/e2e/verified-deprecated.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index f90d9db554c..9213d2b05d9 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/e2e/warning-deprecated.svg b/res/img/e2e/warning-deprecated.svg new file mode 100644 index 00000000000..58f5c3b7d1c --- /dev/null +++ b/res/img/e2e/warning-deprecated.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 58f5c3b7d1c..1acbb53bb71 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/element-icons/settings/inactive.svg b/res/img/element-icons/settings/inactive.svg new file mode 100644 index 00000000000..63b6b97bd59 --- /dev/null +++ b/res/img/element-icons/settings/inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/dropdown-arrow.svg b/res/img/feather-customised/dropdown-arrow.svg index e807f9ca8c5..24645d2bbaa 100644 --- a/res/img/feather-customised/dropdown-arrow.svg +++ b/res/img/feather-customised/dropdown-arrow.svg @@ -1,5 +1,5 @@ - + diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index fb97e2cf362..ba19d616ace 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -239,6 +239,8 @@ $e2e-verified-color: #76cfa5; /* N.B. *NOT* the same as $accent */ $e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; +$e2e-verified-color-light: rgba($e2e-verified-color, 0.06); +$e2e-warning-color-light: rgba($e2e-warning-color, 0.06); /*** ImageView ***/ $lightbox-bg-color: #424242; diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 8549c1b8fe3..67d313da3cf 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -210,6 +210,8 @@ $e2e-verified-color: #76cfa5; /* N.B. *NOT* the same as $accent */ $e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; +$e2e-verified-color-light: rgba($e2e-verified-color, 0.06); +$e2e-warning-color-light: rgba($e2e-warning-color, 0.06); /* ******************** */ /* Tabbed views */ diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7c595640fd0..00758371112 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -23,7 +23,7 @@ import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; -import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; +import { RoomListStore } from "../stores/room-list/Interface"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import { IntegrationManagers } from "../integrations/IntegrationManagers"; @@ -79,7 +79,7 @@ declare global { mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; - mxRoomListStore: RoomListStoreClass; + mxRoomListStore: RoomListStore; mxRoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index abe3a8ce592..1e59eec2e2e 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -648,13 +648,13 @@ export function topicToHtml( emojiBodyElements = formatEmojis(topic, false); } - return isFormattedTopic ? - : + /> + : { emojiBodyElements || topic } ; } diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 10530d7c9b4..9310391e3e2 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -434,29 +434,29 @@ function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null // Currently will only display a change if a user's power level is changed function textForPowerEvent(event: MatrixEvent): () => string | null { const senderName = getSenderName(event); - if (!event.getPrevContent() || !event.getPrevContent().users || - !event.getContent() || !event.getContent().users) { + if (!event.getPrevContent()?.users || !event.getContent()?.users) { return null; } - const previousUserDefault = event.getPrevContent().users_default || 0; - const currentUserDefault = event.getContent().users_default || 0; + const previousUserDefault: number = event.getPrevContent().users_default || 0; + const currentUserDefault: number = event.getContent().users_default || 0; // Construct set of userIds - const users = []; - Object.keys(event.getContent().users).forEach( - (userId) => { - if (users.indexOf(userId) === -1) users.push(userId); - }, - ); - Object.keys(event.getPrevContent().users).forEach( - (userId) => { - if (users.indexOf(userId) === -1) users.push(userId); - }, - ); - - const diffs = []; + const users: string[] = []; + Object.keys(event.getContent().users).forEach((userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + }); + Object.keys(event.getPrevContent().users).forEach((userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + }); + + const diffs: { + userId: string; + name: string; + from: number; + to: number; + }[] = []; users.forEach((userId) => { // Previous power level - let from = event.getPrevContent().users[userId]; + let from: number = event.getPrevContent().users[userId]; if (!Number.isInteger(from)) { from = previousUserDefault; } diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index d2134c33fef..9a6f5b26a0a 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,18 +15,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { HTMLAttributes, WheelEvent } from "react"; +import classNames from "classnames"; +import React, { HTMLAttributes, ReactHTML, WheelEvent } from "react"; -interface IProps extends Omit, "onScroll"> { +type DynamicHtmlElementProps = + JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; +type DynamicElementProps = Partial>; + +export type IProps = DynamicHtmlElementProps & { + element?: T; className?: string; onScroll?: (event: Event) => void; onWheel?: (event: WheelEvent) => void; style?: React.CSSProperties; tabIndex?: number; wrappedRef?: (ref: HTMLDivElement) => void; -} +}; + +export default class AutoHideScrollbar extends React.Component> { + static defaultProps = { + element: 'div' as keyof ReactHTML, + }; -export default class AutoHideScrollbar extends React.Component { public readonly containerRef: React.RefObject = React.createRef(); public componentDidMount() { @@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component { this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true }); } - if (this.props.wrappedRef) { - this.props.wrappedRef(this.containerRef.current); - } + this.props.wrappedRef?.(this.containerRef.current); } public componentWillUnmount() { @@ -49,19 +57,15 @@ export default class AutoHideScrollbar extends React.Component { public render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props; + const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; - return (
- { children } -
); + tabIndex: tabIndex ?? -1, + }, children); } } diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index dc64dd23518..2445e0b38aa 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -225,35 +225,57 @@ export default class ContextMenu extends React.PureComponent { protected renderMenu(hasBackground = this.props.hasBackground) { const position: Partial> = {}; - const props = this.props; - - if (props.top) { - position.top = props.top; + const { + top, + bottom, + left, + right, + bottomAligned, + rightAligned, + menuClassName, + menuHeight, + menuWidth, + menuPaddingLeft, + menuPaddingRight, + menuPaddingBottom, + menuPaddingTop, + zIndex, + children, + focusLock, + managed, + wrapperClassName, + chevronFace: propsChevronFace, + chevronOffset: propsChevronOffset, + ...props + } = this.props; + + if (top) { + position.top = top; } else { - position.bottom = props.bottom; + position.bottom = bottom; } let chevronFace: ChevronFace; - if (props.left) { - position.left = props.left; + if (left) { + position.left = left; chevronFace = ChevronFace.Left; } else { - position.right = props.right; + position.right = right; chevronFace = ChevronFace.Right; } const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null; const chevronOffset: CSSProperties = {}; - if (props.chevronFace) { - chevronFace = props.chevronFace; + if (propsChevronFace) { + chevronFace = propsChevronFace; } const hasChevron = chevronFace && chevronFace !== ChevronFace.None; if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) { - chevronOffset.left = props.chevronOffset; + chevronOffset.left = propsChevronOffset; } else { - chevronOffset.top = props.chevronOffset; + chevronOffset.top = propsChevronOffset; } // If we know the dimensions of the context menu, adjust its position to @@ -262,13 +284,13 @@ export default class ContextMenu extends React.PureComponent { if (contextMenuRect) { if (position.top !== undefined) { let maxTop = windowHeight - WINDOW_PADDING; - if (!this.props.bottomAligned) { + if (!bottomAligned) { maxTop -= contextMenuRect.height; } position.top = Math.min(position.top, maxTop); // Adjust the chevron if necessary if (chevronOffset.top !== undefined) { - chevronOffset.top = props.chevronOffset + props.top - position.top; + chevronOffset.top = propsChevronOffset + top - position.top; } } else if (position.bottom !== undefined) { position.bottom = Math.min( @@ -276,17 +298,17 @@ export default class ContextMenu extends React.PureComponent { windowHeight - contextMenuRect.height - WINDOW_PADDING, ); if (chevronOffset.top !== undefined) { - chevronOffset.top = props.chevronOffset + position.bottom - props.bottom; + chevronOffset.top = propsChevronOffset + position.bottom - bottom; } } if (position.left !== undefined) { let maxLeft = windowWidth - WINDOW_PADDING; - if (!this.props.rightAligned) { + if (!rightAligned) { maxLeft -= contextMenuRect.width; } position.left = Math.min(position.left, maxLeft); if (chevronOffset.left !== undefined) { - chevronOffset.left = props.chevronOffset + props.left - position.left; + chevronOffset.left = propsChevronOffset + left - position.left; } } else if (position.right !== undefined) { position.right = Math.min( @@ -294,7 +316,7 @@ export default class ContextMenu extends React.PureComponent { windowWidth - contextMenuRect.width - WINDOW_PADDING, ); if (chevronOffset.left !== undefined) { - chevronOffset.left = props.chevronOffset + position.right - props.right; + chevronOffset.left = propsChevronOffset + position.right - right; } } } @@ -320,36 +342,36 @@ export default class ContextMenu extends React.PureComponent { 'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right, 'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top, 'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom, - 'mx_ContextualMenu_rightAligned': this.props.rightAligned === true, - 'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true, - }, this.props.menuClassName); + 'mx_ContextualMenu_rightAligned': rightAligned === true, + 'mx_ContextualMenu_bottomAligned': bottomAligned === true, + }, menuClassName); const menuStyle: CSSProperties = {}; - if (props.menuWidth) { - menuStyle.width = props.menuWidth; + if (menuWidth) { + menuStyle.width = menuWidth; } - if (props.menuHeight) { - menuStyle.height = props.menuHeight; + if (menuHeight) { + menuStyle.height = menuHeight; } - if (!isNaN(Number(props.menuPaddingTop))) { - menuStyle["paddingTop"] = props.menuPaddingTop; + if (!isNaN(Number(menuPaddingTop))) { + menuStyle["paddingTop"] = menuPaddingTop; } - if (!isNaN(Number(props.menuPaddingLeft))) { - menuStyle["paddingLeft"] = props.menuPaddingLeft; + if (!isNaN(Number(menuPaddingLeft))) { + menuStyle["paddingLeft"] = menuPaddingLeft; } - if (!isNaN(Number(props.menuPaddingBottom))) { - menuStyle["paddingBottom"] = props.menuPaddingBottom; + if (!isNaN(Number(menuPaddingBottom))) { + menuStyle["paddingBottom"] = menuPaddingBottom; } - if (!isNaN(Number(props.menuPaddingRight))) { - menuStyle["paddingRight"] = props.menuPaddingRight; + if (!isNaN(Number(menuPaddingRight))) { + menuStyle["paddingRight"] = menuPaddingRight; } const wrapperStyle = {}; - if (!isNaN(Number(props.zIndex))) { - menuStyle["zIndex"] = props.zIndex + 1; - wrapperStyle["zIndex"] = props.zIndex; + if (!isNaN(Number(zIndex))) { + menuStyle["zIndex"] = zIndex + 1; + wrapperStyle["zIndex"] = zIndex; } let background; @@ -366,10 +388,10 @@ export default class ContextMenu extends React.PureComponent { let body = <> { chevron } - { props.children } + { children } ; - if (props.focusLock) { + if (focusLock) { body = { body } ; @@ -379,7 +401,7 @@ export default class ContextMenu extends React.PureComponent { { ({ onKeyDownHandler }) => (
{ className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} - role={this.props.managed ? "menu" : undefined} + role={managed ? "menu" : undefined} + {...props} > { body }
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 200c28d159f..b2596cee435 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -85,7 +85,7 @@ const UserWelcomeTop = () => {

{ _tDom("Welcome %(name)s", { name: ownProfile.displayName }) }

-

{ _tDom("Now, let's help you get started") }

+

{ _tDom("Now, let's help you get started") }

; }; @@ -97,8 +97,8 @@ const HomePage: React.FC = ({ justRegistered = false }) => { return ; } - let introSection; - if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { + let introSection: JSX.Element; + if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { introSection = ; } else { const brandingConfig = SdkConfig.getObject("branding"); @@ -107,11 +107,11 @@ const HomePage: React.FC = ({ justRegistered = false }) => { introSection = {config.brand}

{ _tDom("Welcome to %(appName)s", { appName: config.brand }) }

-

{ _tDom("Own your conversations.") }

+

{ _tDom("Own your conversations.") }

; } - return + return
{ introSection }
diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 4b122345b32..ea876f9ae29 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, createRef } from "react"; +import React, { createRef } from "react"; -import AutoHideScrollbar from "./AutoHideScrollbar"; +import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; -interface IProps extends Omit, "onWheel"> { +export type IProps = Omit, "onWheel"> & { // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning // by the parent element. @@ -31,21 +31,22 @@ interface IProps extends Omit, "onWheel verticalScrollsHorizontally?: boolean; children: React.ReactNode; - className: string; -} +}; interface IState { leftIndicatorOffset: string; rightIndicatorOffset: string; } -export default class IndicatorScrollbar extends React.Component { - private autoHideScrollbar = createRef(); +export default class IndicatorScrollbar< + T extends keyof JSX.IntrinsicElements, +> extends React.Component, IState> { + private autoHideScrollbar = createRef>(); private scrollElement: HTMLDivElement; private likelyTrackpadUser: boolean = null; private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser - constructor(props: IProps) { + constructor(props: IProps) { super(props); this.state = { @@ -65,7 +66,7 @@ export default class IndicatorScrollbar extends React.Component } }; - public componentDidUpdate(prevProps: IProps): void { + public componentDidUpdate(prevProps: IProps): void { const prevLen = React.Children.count(prevProps.children); const curLen = React.Children.count(this.props.children); // check overflow only if amount of children changes. diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 26cd8391d65..93ef08ae596 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -45,9 +45,12 @@ import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { UIComponent } from "../../settings/UIFeature"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import PosthogTrackers from "../../PosthogTrackers"; +import PageType from "../../PageTypes"; +import { UserOnboardingButton } from "../views/user-onboarding/UserOnboardingButton"; interface IProps { isMinimized: boolean; + pageType: PageType; resizeNotifier: ResizeNotifier; } @@ -392,6 +395,10 @@ export default class LeftPanel extends React.Component { onVisibilityChange={this.refreshStickyHeaders} /> ) } +
{ data-collapsed={this.props.collapseLhs ? true : undefined} > diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 2f5bd8b05dd..1a925ecc40c 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -504,7 +504,7 @@ export const useRoomHierarchy = (space: Room): { loadMore(pageSize?: number): Promise; } => { const [rooms, setRooms] = useState([]); - const [hierarchy, setHierarchy] = useState(); + const [roomHierarchy, setHierarchy] = useState(); const [error, setError] = useState(); const resetHierarchy = useCallback(() => { @@ -526,15 +526,21 @@ export const useRoomHierarchy = (space: Room): { })); const loadMore = useCallback(async (pageSize?: number) => { - if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport || error) return; - await hierarchy.load(pageSize).catch(setError); - setRooms(hierarchy.rooms); - }, [error, hierarchy]); + if (roomHierarchy.loading || !roomHierarchy.canLoadMore || roomHierarchy.noSupport || error) return; + await roomHierarchy.load(pageSize).catch(setError); + setRooms(roomHierarchy.rooms); + }, [error, roomHierarchy]); + + // Only return the hierarchy if it is for the space requested + let hierarchy = roomHierarchy; + if (hierarchy?.root !== space) { + hierarchy = undefined; + } return { loading: hierarchy?.loading ?? true, rooms, - hierarchy: hierarchy?.root === space ? hierarchy : undefined, + hierarchy, loadMore, error, }; diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index e3dc77a4665..b2c7544f1fd 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -17,17 +17,19 @@ limitations under the License. import React from 'react'; import { Room } from "matrix-js-sdk/src/models/room"; import filesize from "filesize"; -import { IEventRelation } from 'matrix-js-sdk/src/matrix'; +import { IAbortablePromise, IEventRelation } from 'matrix-js-sdk/src/matrix'; +import { Optional } from "matrix-events-sdk"; import ContentMessages from '../../ContentMessages'; import dis from "../../dispatcher/dispatcher"; import { _t } from '../../languageHandler'; -import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import ProgressBar from "../views/elements/ProgressBar"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import { IUpload } from "../../models/IUpload"; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { ActionPayload } from '../../dispatcher/payloads'; +import { UploadPayload } from "../../dispatcher/payloads/UploadPayload"; interface IProps { room: Room; @@ -35,23 +37,35 @@ interface IProps { } interface IState { - currentUpload?: IUpload; - uploadsHere: IUpload[]; + currentFile?: string; + currentPromise?: IAbortablePromise; + currentLoaded?: number; + currentTotal?: number; + countFiles: number; } -export default class UploadBar extends React.Component { +function isUploadPayload(payload: ActionPayload): payload is UploadPayload { + return [ + Action.UploadStarted, + Action.UploadProgress, + Action.UploadFailed, + Action.UploadFinished, + Action.UploadCanceled, + ].includes(payload.action as Action); +} + +export default class UploadBar extends React.PureComponent { static contextType = MatrixClientContext; - private dispatcherRef: string; - private mounted: boolean; + private dispatcherRef: Optional; + private mounted = false; constructor(props) { super(props); // Set initial state to any available upload in this room - we might be mounting // earlier than the first progress event, so should show something relevant. - const uploadsHere = this.getUploadsInRoom(); - this.state = { currentUpload: uploadsHere[0], uploadsHere }; + this.state = this.calculateState(); } componentDidMount() { @@ -61,7 +75,7 @@ export default class UploadBar extends React.Component { componentWillUnmount() { this.mounted = false; - dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef!); } private getUploadsInRoom(): IUpload[] { @@ -69,45 +83,48 @@ export default class UploadBar extends React.Component { return uploads.filter(u => u.roomId === this.props.room.roomId); } + private calculateState(): IState { + const [currentUpload, ...otherUploads] = this.getUploadsInRoom(); + return { + currentFile: currentUpload?.fileName, + currentPromise: currentUpload?.promise, + currentLoaded: currentUpload?.loaded, + currentTotal: currentUpload?.total, + countFiles: otherUploads.length + 1, + }; + } + private onAction = (payload: ActionPayload) => { - switch (payload.action) { - case Action.UploadStarted: - case Action.UploadProgress: - case Action.UploadFinished: - case Action.UploadCanceled: - case Action.UploadFailed: { - if (!this.mounted) return; - const uploadsHere = this.getUploadsInRoom(); - this.setState({ currentUpload: uploadsHere[0], uploadsHere }); - break; - } + if (!this.mounted) return; + if (isUploadPayload(payload)) { + this.setState(this.calculateState()); } }; - private onCancelClick = (ev) => { + private onCancelClick = (ev: ButtonEvent) => { ev.preventDefault(); - ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context); + ContentMessages.sharedInstance().cancelUpload(this.state.currentPromise!, this.context); }; render() { - if (!this.state.currentUpload) { + if (!this.state.currentFile) { return null; } // MUST use var name 'count' for pluralization to kick in const uploadText = _t( "Uploading %(filename)s and %(count)s others", { - filename: this.state.currentUpload.fileName, - count: this.state.uploadsHere.length - 1, + filename: this.state.currentFile, + count: this.state.countFiles - 1, }, ); - const uploadSize = filesize(this.state.currentUpload.total); + const uploadSize = filesize(this.state.currentTotal!); return (
{ uploadText } ({ uploadSize })
- +
); } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 606f96553d7..6251faee41f 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC = ({ const cli = useContext(MatrixClientContext); const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]); - const scrollRef = useRef(); + const scrollRef = useRef>(); const [scrollState, setScrollState] = useState({ // these are estimates which update as soon as it mounts scrollTop: 0, diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index 8039a67511e..f0f1abb3c1c 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -58,10 +58,10 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =
onFinished("legacy")}> - { _t("Manually Verify by Text") } + { _t("Manually verify by text") } onFinished("sas")}> - { _t("Interactively verify by Emoji") } + { _t("Interactively verify by emoji") } onFinished(false)}> { _t("Done") } diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 1a945405023..63d9ad1d2cd 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -24,9 +24,9 @@ import { IDialogProps } from "../IDialogProps"; function iconFromPhase(phase: Phase) { if (phase === Phase.Done) { - return require("../../../../../res/img/e2e/verified.svg").default; + return require("../../../../../res/img/e2e/verified-deprecated.svg").default; } else { - return require("../../../../../res/img/e2e/warning.svg").default; + return require("../../../../../res/img/e2e/warning-deprecated.svg").default; } } diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index dd2b9232111..3d8b16c1f4f 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -376,7 +376,9 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n })), ...roomResults, ...userResults, - ...(profile ? [new DirectoryMember(profile)] : []).map(toMemberResult), + ...(profile && !alreadyAddedUserIds.has(profile.user_id) + ? [new DirectoryMember(profile)] + : []).map(toMemberResult), ...publicRooms.map(toPublicRoomResult), ].filter(result => filter === null || result.filter.includes(filter)); }, diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index f54a8d4bff5..a2337444cfa 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -33,7 +33,8 @@ type AccessibleButtonKind = | 'primary' | 'link_inline' | 'link_sm' | 'confirm_sm' - | 'cancel_sm'; + | 'cancel_sm' + | 'icon'; /** * This type construct allows us to specifically pass those props down to the element we’re creating that the element diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 05c67ab9413..211966bde65 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -192,7 +192,7 @@ export default class AppTile extends React.Component { } private onMyMembership = (room: Room, membership: string): void => { - if (membership === "leave" && room.roomId === this.props.room?.roomId) { + if ((membership === "leave" || membership === "ban") && room.roomId === this.props.room?.roomId) { this.onUserLeftRoom(); } }; diff --git a/src/components/views/elements/ProgressBar.tsx b/src/components/views/elements/ProgressBar.tsx index af06f579ead..2759846ffe4 100644 --- a/src/components/views/elements/ProgressBar.tsx +++ b/src/components/views/elements/ProgressBar.tsx @@ -25,10 +25,10 @@ interface IProps { } const PROGRESS_BAR_ANIMATION_DURATION = 300; -const ProgressBar: React.FC = ({ value, max, animated }) => { +const ProgressBar: React.FC = ({ value, max, animated = true }) => { // Animating progress bars via CSS transition isn’t possible in all of our supported browsers yet. // As workaround, we’re using animations through JS requestAnimationFrame - const currentValue = useSmoothAnimation(0, value, PROGRESS_BAR_ANIMATION_DURATION, animated); + const currentValue = useSmoothAnimation(0, value, animated ? PROGRESS_BAR_ANIMATION_DURATION : 0); return ; }; diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 3b7e6c96701..ae1aff26def 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -52,8 +52,10 @@ export interface ITooltipProps { maxParentWidth?: number; } -export default class Tooltip extends React.Component { - private tooltipContainer: HTMLElement; +type State = Partial>; + +export default class Tooltip extends React.PureComponent { + private static container: HTMLElement; private parent: Element; // XXX: This is because some components (Field) are unable to `import` the Tooltip class, @@ -65,37 +67,47 @@ export default class Tooltip extends React.Component { alignment: Alignment.Natural, }; - // Create a wrapper for the tooltip outside the parent and attach it to the body element + constructor(props) { + super(props); + + this.state = {}; + + // Create a wrapper for the tooltips and attach it to the body element + if (!Tooltip.container) { + Tooltip.container = document.createElement("div"); + Tooltip.container.className = "mx_Tooltip_wrapper"; + document.body.appendChild(Tooltip.container); + } + } + public componentDidMount() { - this.tooltipContainer = document.createElement("div"); - this.tooltipContainer.className = "mx_Tooltip_wrapper"; - document.body.appendChild(this.tooltipContainer); - window.addEventListener('scroll', this.renderTooltip, { + window.addEventListener('scroll', this.updatePosition, { passive: true, capture: true, }); this.parent = ReactDOM.findDOMNode(this).parentNode as Element; - this.renderTooltip(); + this.updatePosition(); } public componentDidUpdate() { - this.renderTooltip(); + this.updatePosition(); } // Remove the wrapper element, as the tooltip has finished using it public componentWillUnmount() { - ReactDOM.unmountComponentAtNode(this.tooltipContainer); - document.body.removeChild(this.tooltipContainer); - window.removeEventListener('scroll', this.renderTooltip, { + window.removeEventListener('scroll', this.updatePosition, { capture: true, }); } // Add the parent's position to the tooltips, so it's correctly // positioned, also taking into account any window zoom - private updatePosition(style: CSSProperties) { + private updatePosition = (): void => { + // When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance) + if (!this.props.visible) return; + const parentBox = this.parent.getBoundingClientRect(); const width = UIStore.instance.windowWidth; const spacing = 6; @@ -112,6 +124,7 @@ export default class Tooltip extends React.Component { parentBox.left - window.scrollX + (parentWidth / 2) ); + const style: State = {}; switch (this.props.alignment) { case Alignment.Natural: if (parentBox.right > width / 2) { @@ -153,25 +166,20 @@ export default class Tooltip extends React.Component { break; } - return style; - } - - private renderTooltip = () => { - let style: CSSProperties = {}; - // When the tooltip is hidden, no need to thrash the DOM with `style` - // attribute updates (performance) - if (this.props.visible) { - style = this.updatePosition({}); - } - // Hide the entire container when not visible. This prevents flashing of the tooltip - // if it is not meant to be visible on first mount. - style.display = this.props.visible ? "block" : "none"; + this.setState(style); + }; + public render() { const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, { "mx_Tooltip_visible": this.props.visible, "mx_Tooltip_invisible": !this.props.visible, }); + const style = { ...this.state }; + // Hide the entire container when not visible. + // This prevents flashing of the tooltip if it is not meant to be visible on first mount. + style.display = this.props.visible ? "block" : "none"; + const tooltip = (
@@ -179,14 +187,10 @@ export default class Tooltip extends React.Component {
); - // Render the tooltip manually, as we wish it not to be rendered within the parent - ReactDOM.render(tooltip, this.tooltipContainer); - }; - - public render() { - // Render a placeholder return ( -
+
+ { ReactDOM.createPortal(tooltip, Tooltip.container) } +
); } } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 495367ba556..e2778333f92 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -62,7 +62,7 @@ class EmojiPicker extends React.Component { private readonly categories: ICategory[]; private readonly shortcodes_to_custom_emoji: Record = {}; - private scrollRef = React.createRef(); + private scrollRef = React.createRef>(); constructor(props: IProps) { super(props); @@ -291,7 +291,7 @@ class EmojiPicker extends React.Component { render() { let heightBefore = 0; return ( -
+
{ isEmojiDisabled={this.isEmojiDisabled} selectedEmojis={this.state.selectedEmojis} showQuickReactions={true} - data-testid='mx_ReactionPicker' />; } } diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 9d51c61074f..c5108051160 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement, useContext, useEffect } from 'react'; +import React, { ReactElement, useCallback, useContext, useEffect } from 'react'; import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event'; import classNames from 'classnames'; import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event'; @@ -88,7 +88,7 @@ const OptionsButton: React.FC = ({ onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); - const onOptionsClick = (e: React.MouseEvent): void => { + const onOptionsClick = useCallback((e: React.MouseEvent): void => { // Don't open the regular browser or our context menu on right-click e.preventDefault(); e.stopPropagation(); @@ -97,7 +97,7 @@ const OptionsButton: React.FC = ({ // the element that is currently focused is skipped. So we want to call onFocus manually to keep the // position in the page even when someone is clicking around. onFocus(); - }; + }, [openMenu, onFocus]); let contextMenu: ReactElement | null; if (menuDisplayed) { @@ -121,6 +121,7 @@ const OptionsButton: React.FC = ({ className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton" title={_t("Options")} onClick={onOptionsClick} + onContextMenu={onOptionsClick} isExpanded={menuDisplayed} inputRef={ref} onFocus={onFocus} @@ -153,17 +154,24 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC ; } + const onClick = useCallback((e: React.MouseEvent) => { + // Don't open the regular browser or our context menu on right-click + e.preventDefault(); + e.stopPropagation(); + + openMenu(); + // when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks + // the element that is currently focused is skipped. So we want to call onFocus manually to keep the + // position in the page even when someone is clicking around. + onFocus(); + }, [openMenu, onFocus]); + return { - openMenu(); - // when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks - // the element that is currently focused is skipped. So we want to call onFocus manually to keep the - // position in the page even when someone is clicking around. - onFocus(); - }} + onClick={onClick} + onContextMenu={onClick} isExpanded={menuDisplayed} inputRef={ref} onFocus={onFocus} @@ -193,7 +201,11 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => { return null; } - const onClick = (): void => { + const onClick = (e: React.MouseEvent): void => { + // Don't open the regular browser or our context menu on right-click + e.preventDefault(); + e.stopPropagation(); + if (firstTimeSeeingThreads) { localStorage.setItem("mx_seen_feature_thread", "true"); } @@ -245,6 +257,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => { : _t("Can't create a thread from an event with an existing relation")} onClick={onClick} + onContextMenu={onClick} > { firstTimeSeeingThreads && !threadsEnabled && ( @@ -265,10 +278,19 @@ const FavouriteButton = ({ mxEvent }: IFavouriteButtonProp) => { 'mx_MessageActionBar_favouriteButton_fillstar': isFavourite(eventId), }); + const onClick = useCallback((e: React.MouseEvent) => { + // Don't open the regular browser or our context menu on right-click + e.preventDefault(); + e.stopPropagation(); + + toggleFavourite(eventId); + }, [toggleFavourite, eventId]); + return toggleFavourite(eventId)} + onClick={onClick} + onContextMenu={onClick} data-testid={eventId} > @@ -335,7 +357,11 @@ export default class MessageActionBar extends React.PureComponent { + private onReplyClick = (e: React.MouseEvent): void => { + // Don't open the regular browser or our context menu on right-click + e.preventDefault(); + e.stopPropagation(); + dis.dispatch({ action: 'reply_to_event', event: this.props.mxEvent, @@ -343,7 +369,11 @@ export default class MessageActionBar extends React.PureComponent { + private onEditClick = (e: React.MouseEvent): void => { + // Don't open the regular browser or our context menu on right-click + e.preventDefault(); + e.stopPropagation(); + editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent); }; @@ -406,6 +436,10 @@ export default class MessageActionBar extends React.PureComponent { + // Don't open the regular browser or our context menu on right-click + ev.preventDefault(); + ev.stopPropagation(); + this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv)); }; @@ -423,6 +457,7 @@ export default class MessageActionBar extends React.PureComponent @@ -433,6 +468,7 @@ export default class MessageActionBar extends React.PureComponent @@ -453,6 +489,7 @@ export default class MessageActionBar extends React.PureComponent @@ -475,6 +512,7 @@ export default class MessageActionBar extends React.PureComponent diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 1de0febdc23..3bb5eabbb25 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -59,11 +59,30 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps; } +const baseBodyTypes = new Map([ + [MsgType.Text, TextualBody], + [MsgType.Notice, TextualBody], + [MsgType.Emote, TextualBody], + [MsgType.Image, MImageBody], + [MsgType.File, MFileBody], + [MsgType.Audio, MVoiceOrAudioBody], + [MsgType.Video, MVideoBody], +]); +const baseEvTypes = new Map>>([ + [EventType.Sticker, MStickerBody], + [M_POLL_START.name, MPollBody], + [M_POLL_START.altName, MPollBody], + [M_BEACON_INFO.name, MBeaconBody], + [M_BEACON_INFO.altName, MBeaconBody], +]); + export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper: MediaEventHelper; + private bodyTypes = new Map(baseBodyTypes.entries()); + private evTypes = new Map>>(baseEvTypes.entries()); - static contextType = MatrixClientContext; + public static contextType = MatrixClientContext; public context!: React.ContextType; public constructor(props: IProps, context: React.ContextType) { @@ -72,6 +91,8 @@ export default class MessageEvent extends React.Component implements IMe if (MediaEventHelper.isEligible(this.props.mxEvent)) { this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } + + this.updateComponentMaps(); } public componentDidMount(): void { @@ -88,32 +109,20 @@ export default class MessageEvent extends React.Component implements IMe this.mediaHelper?.destroy(); this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } - } - private get bodyTypes(): Record { - return { - [MsgType.Text]: TextualBody, - [MsgType.Notice]: TextualBody, - [MsgType.Emote]: TextualBody, - [MsgType.Image]: MImageBody, - [MsgType.File]: MFileBody, - [MsgType.Audio]: MVoiceOrAudioBody, - [MsgType.Video]: MVideoBody, - - ...(this.props.overrideBodyTypes || {}), - }; + this.updateComponentMaps(); } - private get evTypes(): Record>> { - return { - [EventType.Sticker]: MStickerBody, - [M_POLL_START.name]: MPollBody, - [M_POLL_START.altName]: MPollBody, - [M_BEACON_INFO.name]: MBeaconBody, - [M_BEACON_INFO.altName]: MBeaconBody, + private updateComponentMaps() { + this.bodyTypes = new Map(baseBodyTypes.entries()); + for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) { + this.bodyTypes.set(bodyType, bodyComponent); + } - ...(this.props.overrideEventTypes || {}), - }; + this.evTypes = new Map>>(baseEvTypes.entries()); + for (const [evType, evComponent] of Object.entries(this.props.overrideEventTypes ?? {})) { + this.evTypes.set(evType, evComponent); + } } public getEventTileOps = () => { @@ -143,13 +152,13 @@ export default class MessageEvent extends React.Component implements IMe let BodyType: React.ComponentType> | ReactAnyComponent = RedactedBody; if (!this.props.mxEvent.isRedacted()) { // only resolve BodyType if event is not redacted - if (type && this.evTypes[type]) { - BodyType = this.evTypes[type]; - } else if (msgtype && this.bodyTypes[msgtype]) { - BodyType = this.bodyTypes[msgtype]; + if (type && this.evTypes.has(type)) { + BodyType = this.evTypes.get(type); + } else if (msgtype && this.bodyTypes.has(msgtype)) { + BodyType = this.bodyTypes.get(msgtype); } else if (content.url) { // Fallback to MFileBody if there's a content URL - BodyType = this.bodyTypes[MsgType.File]; + BodyType = this.bodyTypes.get(MsgType.File); } else { // Fallback to UnknownBody otherwise if not redacted BodyType = UnknownBody; diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index a9ab3e6ab38..d0d6c7bf5b9 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -85,7 +85,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) { Modal.createDialog(ErrorDialog, { - headerImage: require("../../../../res/img/e2e/warning.svg").default, + headerImage: require("../../../../res/img/e2e/warning-deprecated.svg").default, title: _t("Your messages are not secure"), description:
{ _t("One of the following may be compromised:") } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8bad315cfe4..ad4dfd7b55f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -870,9 +870,6 @@ export class UnwrappedEventTile extends React.Component { private showContextMenu(ev: React.MouseEvent, permalink?: string): void { const clickTarget = ev.target as HTMLElement; - // Return if message right-click context menu isn't enabled - if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return; - // Try to find an anchor element const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a"); diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx index b30f832b8b8..83ef49485b8 100644 --- a/src/components/views/rooms/LinkPreviewWidget.tsx +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -121,7 +121,13 @@ export default class LinkPreviewWidget extends React.Component { let img; if (image) { img =
- +
; } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 38f8ddebad1..baf36b0f13b 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -388,7 +388,7 @@ export default class RoomSublist extends React.Component { }; private onTagSortChanged = async (sort: SortAlgorithm) => { - await RoomListStore.instance.setTagSorting(this.props.tagId, sort); + RoomListStore.instance.setTagSorting(this.props.tagId, sort); this.forceUpdate(); }; diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index 5ae034d9fec..f32f7997fed 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -22,12 +22,10 @@ import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import Modal from '../../../Modal'; -import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; -import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; import DevicesPanelEntry from "./DevicesPanelEntry"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; +import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices'; interface IProps { className?: string; @@ -79,7 +77,6 @@ export default class DevicesPanel extends React.Component { crossSigningInfo: crossSigningInfo, }; }); - console.log(this.state); }, (error) => { if (this.unmounted) { return; } @@ -178,76 +175,38 @@ export default class DevicesPanel extends React.Component { }); }; - private onDeleteClick = (): void => { + private onDeleteClick = async (): Promise => { if (this.state.selectedDevices.length === 0) { return; } this.setState({ deleting: true, }); - this.makeDeleteRequest(null).catch((error) => { - if (this.unmounted) { return; } - if (error.httpStatus !== 401 || !error.data || !error.data.flows) { - // doesn't look like an interactive-auth failure - throw error; - } - - // pop up an interactive auth dialog - - const numDevices = this.state.selectedDevices.length; - const dialogAesthetics = { - [SSOAuthEntry.PHASE_PREAUTH]: { - title: _t("Use Single Sign On to continue"), - body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", { - count: numDevices, - }), - continueText: _t("Single Sign On"), - continueKind: "primary", - }, - [SSOAuthEntry.PHASE_POSTAUTH]: { - title: _t("Confirm signing out these devices", { - count: numDevices, - }), - body: _t("Click the button below to confirm signing out these devices.", { - count: numDevices, - }), - continueText: _t("Sign out devices", { count: numDevices }), - continueKind: "danger", - }, - }; - Modal.createDialog(InteractiveAuthDialog, { - title: _t("Authentication"), - matrixClient: MatrixClientPeg.get(), - authData: error.data, - makeRequest: this.makeDeleteRequest.bind(this), - aestheticsForStagePhases: { - [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, - [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + try { + await deleteDevicesWithInteractiveAuth( + MatrixClientPeg.get(), + this.state.selectedDevices, + (success) => { + if (success) { + // Reset selection to [], update device list + this.setState({ + selectedDevices: [], + }); + this.loadDevices(); + } + this.setState({ + deleting: false, + }); }, - }); - }).catch((e) => { - logger.error("Error deleting sessions", e); - if (this.unmounted) { return; } - }).finally(() => { + ); + } catch (error) { + logger.error("Error deleting sessions", error); this.setState({ deleting: false, }); - }); + } }; - // TODO: proper typing for auth - private makeDeleteRequest(auth?: any): Promise { - return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then( - () => { - // Reset selection to [], update device list - this.setState({ - selectedDevices: [], - }); - this.loadDevices(); - }, - ); - } - private renderDevice = (device: IMyDevice): JSX.Element => { const myDeviceId = MatrixClientPeg.get().getDeviceId(); const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId)); @@ -289,6 +248,7 @@ export default class DevicesPanel extends React.Component { const myDeviceId = MatrixClientPeg.get().getDeviceId(); const myDevice = devices.find((device) => (device.device_id === myDeviceId)); + if (!myDevice) { return loadError; } @@ -373,6 +333,7 @@ export default class DevicesPanel extends React.Component { onClick={this.onDeleteClick} kind="danger_outline" disabled={this.state.selectedDevices.length === 0} + data-testid='sign-out-devices-btn' > { _t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length }) } ; diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index b0301214b9b..0109c37b9ba 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import { IMyDevice } from 'matrix-js-sdk/src/client'; import { logger } from "matrix-js-sdk/src/logger"; +import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -113,8 +114,6 @@ export default class DevicesPanelEntry extends React.Component { }; public render(): JSX.Element { - const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : ''; - let iconClass = ''; let verifyButton: JSX.Element; if (this.props.verified !== null) { @@ -154,20 +153,25 @@ export default class DevicesPanelEntry extends React.Component { ; + const deviceWithVerification = { + ...this.props.device, + isVerified: this.props.verified, + }; + if (this.props.isOwnDevice) { - return
+ return
- + { buttons }
; } return ( -
- +
+ { buttons }
diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx new file mode 100644 index 00000000000..cebbed64e6b --- /dev/null +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState } from 'react'; + +import { _t } from '../../../../languageHandler'; +import Spinner from '../../elements/Spinner'; +import SettingsSubsection from '../shared/SettingsSubsection'; +import DeviceDetails from './DeviceDetails'; +import DeviceExpandDetailsButton from './DeviceExpandDetailsButton'; +import DeviceTile from './DeviceTile'; +import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; +import { DeviceWithVerification } from './types'; + +interface Props { + device?: DeviceWithVerification; + isLoading: boolean; +} + +const CurrentDeviceSection: React.FC = ({ + device, isLoading, +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + return + { isLoading && } + { !!device && <> + + setIsExpanded(!isExpanded)} + /> + + { isExpanded && } +
+ + + } +
; +}; + +export default CurrentDeviceSection; diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx new file mode 100644 index 00000000000..5a58efaa887 --- /dev/null +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -0,0 +1,81 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { formatDate } from '../../../../DateUtils'; +import { _t } from '../../../../languageHandler'; +import Heading from '../../typography/Heading'; +import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; +import { DeviceWithVerification } from './types'; + +interface Props { + device: DeviceWithVerification; +} + +interface MetadataTable { + heading?: string; + values: { label: string, value?: string | React.ReactNode }[]; +} + +const DeviceDetails: React.FC = ({ device }) => { + const metadata: MetadataTable[] = [ + { + values: [ + { label: _t('Session ID'), value: device.device_id }, + { + label: _t('Last activity'), + value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)), + }, + ], + }, + { + heading: _t('Device'), + values: [ + { label: _t('IP address'), value: device.last_seen_ip }, + ], + }, + ]; + return
+
+ { device.display_name ?? device.device_id } + +
+
+

{ _t('Session details') }

+ { metadata.map(({ heading, values }, index) => + { heading && + + + + } + + + { values.map(({ label, value }) => + + + ) } + +
{ heading }
{ label }{ value }
, + ) } +
+
; +}; + +export default DeviceDetails; diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx new file mode 100644 index 00000000000..a0293fec64f --- /dev/null +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -0,0 +1,43 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import classNames from 'classnames'; +import React from 'react'; + +import { Icon as CaretIcon } from '../../../../../res/img/feather-customised/dropdown-arrow.svg'; +import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; + +interface Props { + isExpanded: boolean; + onClick: () => void; +} + +const DeviceExpandDetailsButton: React.FC = ({ isExpanded, onClick, ...rest }) => { + return + + ; +}; + +export default DeviceExpandDetailsButton; diff --git a/src/components/views/settings/devices/DeviceSecurityCard.tsx b/src/components/views/settings/devices/DeviceSecurityCard.tsx new file mode 100644 index 00000000000..01fe4888821 --- /dev/null +++ b/src/components/views/settings/devices/DeviceSecurityCard.tsx @@ -0,0 +1,55 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import classNames from 'classnames'; +import React from 'react'; + +import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg'; +import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg'; +import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg'; +import { DeviceSecurityVariation } from './types'; +interface Props { + variation: DeviceSecurityVariation; + heading: string; + description: string | React.ReactNode; + children?: React.ReactNode; +} + +const VariationIcon: Record>> = { + [DeviceSecurityVariation.Inactive]: InactiveIcon, + [DeviceSecurityVariation.Verified]: VerifiedIcon, + [DeviceSecurityVariation.Unverified]: UnverifiedIcon, +}; + +const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => { + const Icon = VariationIcon[variation]; + return
+ +
; +}; + +const DeviceSecurityCard: React.FC = ({ variation, heading, description, children }) => { + return
+ +
+

{ heading }

+

{ description }

+ { children } +
+
; +}; + +export default DeviceSecurityCard; diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 33f9fc40a85..c791d2cd259 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -15,21 +15,22 @@ limitations under the License. */ import React, { Fragment } from "react"; -import { IMyDevice } from "matrix-js-sdk/src/matrix"; +import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg'; import { _t } from "../../../../languageHandler"; import { formatDate, formatRelativeTime } from "../../../../DateUtils"; import TooltipTarget from "../../elements/TooltipTarget"; import { Alignment } from "../../elements/Tooltip"; import Heading from "../../typography/Heading"; - +import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "./filter"; +import { DeviceWithVerification } from "./types"; export interface DeviceTileProps { - device: IMyDevice; + device: DeviceWithVerification; children?: React.ReactNode; onClick?: () => void; } -const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { +const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }) => { if (device.display_name) { return = ({ device }) => { ; }; -const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000; +const MS_DAY = 24 * 60 * 60 * 1000; +const MS_6_DAYS = 6 * MS_DAY; const formatLastActivity = (timestamp: number, now = new Date().getTime()): string => { // less than a week ago if (timestamp + MS_6_DAYS >= now) { @@ -56,18 +58,41 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri return formatRelativeTime(new Date(timestamp)); }; -const DeviceMetadata: React.FC<{ value: string, id: string }> = ({ value, id }) => ( +const getInactiveMetadata = (device: DeviceWithVerification): { id: string, value: React.ReactNode } | undefined => { + const isInactive = isDeviceInactive(device); + + if (!isInactive) { + return undefined; + } + return { id: 'inactive', value: ( + <> + + { + _t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) + + ` (${formatLastActivity(device.last_seen_ts)})` + } + ), + }; +}; + +const DeviceMetadata: React.FC<{ value: string | React.ReactNode, id: string }> = ({ value, id }) => ( value ? { value } : null ); const DeviceTile: React.FC = ({ device, children, onClick }) => { + const inactive = getInactiveMetadata(device); const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`; - const metadata = [ - { id: 'lastActivity', value: lastActivity }, - { id: 'lastSeenIp', value: device.last_seen_ip }, - ]; + const verificationStatus = device.isVerified ? _t('Verified') : _t('Unverified'); + // if device is inactive, don't display last activity or verificationStatus + const metadata = inactive + ? [inactive, { id: 'lastSeenIp', value: device.last_seen_ip }] + : [ + { id: 'isVerified', value: verificationStatus }, + { id: 'lastActivity', value: lastActivity }, + { id: 'lastSeenIp', value: device.last_seen_ip }, + ]; - return
+ return
diff --git a/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx new file mode 100644 index 00000000000..a59fd64d638 --- /dev/null +++ b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../../languageHandler'; +import DeviceSecurityCard from './DeviceSecurityCard'; +import { + DeviceSecurityVariation, + DeviceWithVerification, +} from './types'; + +interface Props { + device: DeviceWithVerification; +} + +export const DeviceVerificationStatusCard: React.FC = ({ + device, +}) => { + const securityCardProps = device?.isVerified ? { + variation: DeviceSecurityVariation.Verified, + heading: _t('Verified session'), + description: _t('This session is ready for secure messaging.'), + } : { + variation: DeviceSecurityVariation.Unverified, + heading: _t('Unverified session'), + description: _t('Verify or sign out from this session for best security and reliability.'), + }; + return ; +}; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx new file mode 100644 index 00000000000..5af3d30a366 --- /dev/null +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -0,0 +1,220 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; +import Dropdown from '../../elements/Dropdown'; +import DeviceDetails from './DeviceDetails'; +import DeviceExpandDetailsButton from './DeviceExpandDetailsButton'; +import DeviceSecurityCard from './DeviceSecurityCard'; +import DeviceTile from './DeviceTile'; +import { + filterDevicesBySecurityRecommendation, + INACTIVE_DEVICE_AGE_DAYS, +} from './filter'; +import { + DevicesDictionary, + DeviceSecurityVariation, + DeviceWithVerification, +} from './types'; + +interface Props { + devices: DevicesDictionary; + expandedDeviceIds: DeviceWithVerification['device_id'][]; + filter?: DeviceSecurityVariation; + onFilterChange: (filter: DeviceSecurityVariation | undefined) => void; + onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void; +} + +// devices without timestamp metadata should be sorted last +const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) => + (right.last_seen_ts || 0) - (left.last_seen_ts || 0); + +const getFilteredSortedDevices = (devices: DevicesDictionary, filter: DeviceSecurityVariation) => + filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : []) + .sort(sortDevicesByLatestActivity); + +const ALL_FILTER_ID = 'ALL'; + +const FilterSecurityCard: React.FC<{ filter?: DeviceSecurityVariation | string }> = ({ filter }) => { + switch (filter) { + case DeviceSecurityVariation.Verified: + return
+ +
+ ; + case DeviceSecurityVariation.Unverified: + return
+ +
+ ; + case DeviceSecurityVariation.Inactive: + return
+ +
+ ; + default: + return null; + } +}; + +const getNoResultsMessage = (filter: DeviceSecurityVariation): string => { + switch (filter) { + case DeviceSecurityVariation.Verified: + return _t('No verified sessions found.'); + case DeviceSecurityVariation.Unverified: + return _t('No unverified sessions found.'); + case DeviceSecurityVariation.Inactive: + return _t('No inactive sessions found.'); + default: + return _t('No sessions found.'); + } +}; +interface NoResultsProps { filter: DeviceSecurityVariation, clearFilter: () => void} +const NoResults: React.FC = ({ filter, clearFilter }) => +
+ { getNoResultsMessage(filter) } + { + /* No clear filter button when filter is falsy (ie 'All') */ + !!filter && + <> +   + + { _t('Show all') } + + + } +
; + +const DeviceListItem: React.FC<{ + device: DeviceWithVerification; + isExpanded: boolean; + onDeviceExpandToggle: () => void; +}> = ({ + device, isExpanded, onDeviceExpandToggle, +}) =>
  • + + + + { isExpanded && } +
  • ; + +/** + * Filtered list of devices + * Sorted by latest activity descending + */ +const FilteredDeviceList: React.FC = ({ + devices, + filter, + expandedDeviceIds, + onFilterChange, + onDeviceExpandToggle, +}) => { + const sortedDevices = getFilteredSortedDevices(devices, filter); + + const options = [ + { id: ALL_FILTER_ID, label: _t('All') }, + { + id: DeviceSecurityVariation.Verified, + label: _t('Verified'), + description: _t('Ready for secure messaging'), + }, + { + id: DeviceSecurityVariation.Unverified, + label: _t('Unverified'), + description: _t('Not ready for secure messaging'), + }, + { + id: DeviceSecurityVariation.Inactive, + label: _t('Inactive'), + description: _t( + 'Inactive for %(inactiveAgeDays)s days or longer', + { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }, + ), + }, + ]; + + const onFilterOptionChange = (filterId: DeviceSecurityVariation | typeof ALL_FILTER_ID) => { + onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation); + }; + + return
    +
    + + { _t('Sessions') } + + + { options.map(({ id, label }) => +
    { label }
    , + ) } +
    +
    + { !!sortedDevices.length + ? + : onFilterChange(undefined)} /> + } +
      + { sortedDevices.map((device) => onDeviceExpandToggle(device.device_id)} + />, + ) } +
    +
    + ; +}; + +export default FilteredDeviceList; diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx new file mode 100644 index 00000000000..00181f5674a --- /dev/null +++ b/src/components/views/settings/devices/SecurityRecommendations.tsx @@ -0,0 +1,103 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; +import SettingsSubsection from '../shared/SettingsSubsection'; +import DeviceSecurityCard from './DeviceSecurityCard'; +import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter'; +import { + DeviceSecurityVariation, + DeviceWithVerification, + DevicesDictionary, +} from './types'; + +interface Props { + devices: DevicesDictionary; +} + +const SecurityRecommendations: React.FC = ({ devices }) => { + const devicesArray = Object.values(devices); + + const unverifiedDevicesCount = filterDevicesBySecurityRecommendation( + devicesArray, + [DeviceSecurityVariation.Unverified], + ).length; + const inactiveDevicesCount = filterDevicesBySecurityRecommendation( + devicesArray, + [DeviceSecurityVariation.Inactive], + ).length; + + if (!(unverifiedDevicesCount | inactiveDevicesCount)) { + return null; + } + + const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS; + + // TODO(kerrya) stubbed until PSG-640/652 + const noop = () => {}; + + return + { + !!unverifiedDevicesCount && + + + { _t('View all') + ` (${unverifiedDevicesCount})` } + + + } + { + !!inactiveDevicesCount && + <> + { !!unverifiedDevicesCount &&
    } + + + { _t('View all') + ` (${inactiveDevicesCount})` } + + + + } + ; +}; + +export default SecurityRecommendations; diff --git a/src/components/views/settings/devices/deleteDevices.tsx b/src/components/views/settings/devices/deleteDevices.tsx new file mode 100644 index 00000000000..8decacae78b --- /dev/null +++ b/src/components/views/settings/devices/deleteDevices.tsx @@ -0,0 +1,83 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { IAuthData } from "matrix-js-sdk/src/interactive-auth"; + +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; +import { InteractiveAuthCallback } from "../../../structures/InteractiveAuth"; +import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents"; +import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; + +const makeDeleteRequest = ( + matrixClient: MatrixClient, deviceIds: string[], +) => async (auth?: IAuthData): Promise => { + await matrixClient.deleteMultipleDevices(deviceIds, auth); +}; + +export const deleteDevicesWithInteractiveAuth = async ( + matrixClient: MatrixClient, deviceIds: string[], onFinished?: InteractiveAuthCallback, +) => { + if (!deviceIds.length) { + return; + } + try { + await makeDeleteRequest(matrixClient, deviceIds)(); + // no interactive auth needed + onFinished(true, undefined); + } catch (error) { + if (error.httpStatus !== 401 || !error.data?.flows) { + // doesn't look like an interactive-auth failure + throw error; + } + + // pop up an interactive auth dialog + + const numDevices = deviceIds.length; + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", { + count: numDevices, + }), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm signing out these devices", { + count: numDevices, + }), + body: _t("Click the button below to confirm signing out these devices.", { + count: numDevices, + }), + continueText: _t("Sign out devices", { count: numDevices }), + continueKind: "danger", + }, + }; + Modal.createDialog(InteractiveAuthDialog, { + title: _t("Authentication"), + matrixClient: matrixClient, + authData: error.data, + onFinished, + makeRequest: makeDeleteRequest(matrixClient, deviceIds), + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, + }); + } +}; diff --git a/src/components/views/settings/devices/filter.ts b/src/components/views/settings/devices/filter.ts new file mode 100644 index 00000000000..ad2bc92152c --- /dev/null +++ b/src/components/views/settings/devices/filter.ts @@ -0,0 +1,43 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DeviceWithVerification, DeviceSecurityVariation } from "./types"; + +type DeviceFilterCondition = (device: DeviceWithVerification) => boolean; + +const MS_DAY = 24 * 60 * 60 * 1000; +export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days +export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY; + +export const isDeviceInactive: DeviceFilterCondition = device => + !!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS; + +const filters: Record = { + [DeviceSecurityVariation.Verified]: device => !!device.isVerified, + [DeviceSecurityVariation.Unverified]: device => !device.isVerified, + [DeviceSecurityVariation.Inactive]: isDeviceInactive, +}; + +export const filterDevicesBySecurityRecommendation = ( + devices: DeviceWithVerification[], + securityVariations: DeviceSecurityVariation[], +) => { + const activeFilters = securityVariations.map(variation => filters[variation]); + if (!activeFilters.length) { + return devices; + } + return devices.filter(device => activeFilters.every(filter => filter(device))); +}; diff --git a/src/components/views/settings/devices/types.ts b/src/components/views/settings/devices/types.ts new file mode 100644 index 00000000000..1f3328c09ef --- /dev/null +++ b/src/components/views/settings/devices/types.ts @@ -0,0 +1,26 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IMyDevice } from "matrix-js-sdk/src/matrix"; + +export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null }; +export type DevicesDictionary = Record; + +export enum DeviceSecurityVariation { + Verified = 'Verified', + Unverified = 'Unverified', + Inactive = 'Inactive', +} diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts new file mode 100644 index 00000000000..ec5ee1ca189 --- /dev/null +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useContext, useEffect, useState } from "react"; +import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { logger } from "matrix-js-sdk/src/logger"; + +import MatrixClientContext from "../../../../contexts/MatrixClientContext"; +import { DevicesDictionary } from "./types"; + +const isDeviceVerified = ( + matrixClient: MatrixClient, + crossSigningInfo: CrossSigningInfo, + device: IMyDevice, +): boolean | null => { + try { + const deviceInfo = matrixClient.getStoredDevice(matrixClient.getUserId(), device.device_id); + return crossSigningInfo.checkDeviceTrust( + crossSigningInfo, + deviceInfo, + false, + true, + ).isCrossSigningVerified(); + } catch (error) { + logger.error("Error getting device cross-signing info", error); + return null; + } +}; + +const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise => { + const { devices } = await matrixClient.getDevices(); + const crossSigningInfo = matrixClient.getStoredCrossSigningForUser(matrixClient.getUserId()); + + const devicesDict = devices.reduce((acc, device: IMyDevice) => ({ + ...acc, + [device.device_id]: { + ...device, + isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device), + }, + }), {}); + + return devicesDict; +}; + +export enum OwnDevicesError { + Unsupported = 'Unsupported', + Default = 'Default', +} +type DevicesState = { + devices: DevicesDictionary; + currentDeviceId: string; + isLoading: boolean; + error?: OwnDevicesError; +}; +export const useOwnDevices = (): DevicesState => { + const matrixClient = useContext(MatrixClientContext); + + const currentDeviceId = matrixClient.getDeviceId(); + + const [devices, setDevices] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + const getDevicesAsync = async () => { + setIsLoading(true); + try { + const devices = await fetchDevicesWithVerification(matrixClient); + setDevices(devices); + setIsLoading(false); + } catch (error) { + if (error.httpStatus == 404) { + // 404 probably means the HS doesn't yet support the API. + setError(OwnDevicesError.Unsupported); + } else { + logger.error("Error loading sessions:", error); + setError(OwnDevicesError.Default); + } + setIsLoading(false); + } + }; + getDevicesAsync(); + }, [matrixClient]); + + return { + devices, + currentDeviceId, + isLoading, + error, + }; +}; diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 5dcdc9dad6f..6d23a080caa 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { HTMLAttributes } from "react"; import Heading from "../../typography/Heading"; -export interface SettingsSubsectionProps { +export interface SettingsSubsectionProps extends HTMLAttributes { heading: string; description?: string | React.ReactNode; children?: React.ReactNode; } -const SettingsSubsection: React.FC = ({ heading, description, children }) => ( -
    +const SettingsSubsection: React.FC = ({ heading, description, children, ...rest }) => ( +
    { heading } { !!description &&
    { description }
    }
    diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index ae72c53dd4d..c412453253b 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -46,6 +46,7 @@ interface IState { export default class PreferencesUserSettingsTab extends React.Component { private static ROOM_LIST_SETTINGS = [ 'breadcrumbs', + "FTUE.userOnboardingButton", ]; private static SPACES_SETTINGS = [ diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 17c09aeb7a3..c4878dbb372 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -14,19 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { _t } from "../../../../../languageHandler"; +import { useOwnDevices } from '../../devices/useOwnDevices'; import SettingsSubsection from '../../shared/SettingsSubsection'; +import FilteredDeviceList from '../../devices/FilteredDeviceList'; +import CurrentDeviceSection from '../../devices/CurrentDeviceSection'; +import SecurityRecommendations from '../../devices/SecurityRecommendations'; +import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { + const { devices, currentDeviceId, isLoading } = useOwnDevices(); + const [filter, setFilter] = useState(); + const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); + + const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => { + if (expandedDeviceIds.includes(deviceId)) { + setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId)); + } else { + setExpandedDeviceIds([...expandedDeviceIds, deviceId]); + } + }; + + const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; + const shouldShowOtherSessions = Object.keys(otherDevices).length > 0; + return - + + { + shouldShowOtherSessions && + + + + } ; }; diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index e1f62445f74..129e6f3584e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut "collapsed": isPanelCollapsed, })} role="treeitem" + aria-selected={selected} > ; @@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo(({ style={isDraggingOver ? { pointerEvents: "none", } : undefined} + element="ul" + role="tree" + aria-label={_t("Spaces")} > { metaSpacesSection } { invites.map(s => ( @@ -321,7 +325,7 @@ const InnerSpacePanel = React.memo(({ const SpacePanel = () => { const [isPanelCollapsed, setPanelCollapsed] = useState(true); - const ref = useRef(); + const ref = useRef(); useLayoutEffect(() => { UIStore.instance.trackElementDimensions("SpacePanel", ref.current); return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel"); @@ -340,11 +344,9 @@ const SpacePanel = () => { }}> { ({ onKeyDownHandler }) => ( -
      @@ -381,7 +383,7 @@ const SpacePanel = () => { -
    +
    ) } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 80d678d3b61..cab7bc3c76b 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -315,6 +315,7 @@ export class SpaceItem extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { tabIndex, ...restDragHandleProps } = dragHandleProps || {}; + const selected = activeSpaces.includes(space.roomId); return (
  • { className={itemClasses} ref={innerRef} aria-expanded={hasChildren ? !collapsed : undefined} + aria-selected={selected} role="treeitem" > ("FTUE.useCaseSelection"); + const visible = useSettingValue("FTUE.userOnboardingButton"); + + if (!visible || minimized || !showUserOnboardingPage(useCase)) { + return null; + } + + return ( + + ); +} + +function UserOnboardingButtonInternal({ selected, minimized }: Props) { + const onDismiss = useCallback((ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + PosthogTrackers.trackInteraction("WebRoomListUserOnboardingIgnoreButton", ev); + SettingsStore.setValue("FTUE.userOnboardingButton", null, SettingLevel.ACCOUNT, false); + }, []); + + const onClick = useCallback((ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + PosthogTrackers.trackInteraction("WebRoomListUserOnboardingButton", ev); + defaultDispatcher.fire(Action.ViewHomePage); + }, []); + + return ( + + { !minimized && ( + <> +
    + + { _t("Welcome") } + + +
    + + ) } +
    + ); +} diff --git a/src/components/views/user-onboarding/UserOnboardingFeedback.tsx b/src/components/views/user-onboarding/UserOnboardingFeedback.tsx index 6aabb50f2a9..b6bd03dfe86 100644 --- a/src/components/views/user-onboarding/UserOnboardingFeedback.tsx +++ b/src/components/views/user-onboarding/UserOnboardingFeedback.tsx @@ -32,10 +32,14 @@ export function UserOnboardingFeedback() {
    - { _t("How are you finding Element so far?") } + { _t("How are you finding %(brand)s so far?", { + brand: SdkConfig.get("brand"), + }) }
    - { _t("We’d appreciate any feedback on how you’re finding Element.") } + { _t("We’d appreciate any feedback on how you’re finding %(brand)s.", { + brand: SdkConfig.get("brand"), + }) }
      { tasks.map(([task, completed]) => ( - + )) }
    diff --git a/src/components/views/user-onboarding/UserOnboardingPage.tsx b/src/components/views/user-onboarding/UserOnboardingPage.tsx index 7ca13232986..cc90a3d09d5 100644 --- a/src/components/views/user-onboarding/UserOnboardingPage.tsx +++ b/src/components/views/user-onboarding/UserOnboardingPage.tsx @@ -19,6 +19,7 @@ import * as React from "react"; import { useInitialSyncComplete } from "../../../hooks/useIsInitialSyncComplete"; import { useSettingValue } from "../../../hooks/useSettings"; +import { useUserOnboardingContext } from "../../../hooks/useUserOnboardingContext"; import { useUserOnboardingTasks } from "../../../hooks/useUserOnboardingTasks"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; @@ -47,7 +48,8 @@ export function UserOnboardingPage({ justRegistered = false }: Props) { const pageUrl = getHomePageUrl(config); const useCase = useSettingValue("FTUE.useCaseSelection"); - const [completedTasks, waitingTasks] = useUserOnboardingTasks(); + const context = useUserOnboardingContext(); + const [completedTasks, waitingTasks] = useUserOnboardingTasks(context); const initialSyncComplete = useInitialSyncComplete(); const [showList, setShowList] = useState(false); diff --git a/src/components/views/user-onboarding/UserOnboardingTask.tsx b/src/components/views/user-onboarding/UserOnboardingTask.tsx index 48accab8d32..72c6617ff16 100644 --- a/src/components/views/user-onboarding/UserOnboardingTask.tsx +++ b/src/components/views/user-onboarding/UserOnboardingTask.tsx @@ -27,6 +27,9 @@ interface Props { } export function UserOnboardingTask({ task, completed = false }: Props) { + const title = typeof task.title === "function" ? task.title() : task.title; + const description = typeof task.description === "function" ? task.description() : task.description; + return (
  • - { task.title } + { title }
    - { task.description } + { description }
  • { task.action && (!task.action.hideOnComplete || !completed) && ( diff --git a/src/dispatcher/payloads/UploadPayload.ts b/src/dispatcher/payloads/UploadPayload.ts index 023bd5403ce..7db4a4a4d74 100644 --- a/src/dispatcher/payloads/UploadPayload.ts +++ b/src/dispatcher/payloads/UploadPayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; import { IUpload } from "../../models/IUpload"; -interface UploadPayload extends ActionPayload { +export interface UploadPayload extends ActionPayload { /** * The upload with fields representing the new upload state. */ diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 23e41197238..6be68fd7268 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -75,7 +75,6 @@ export interface EventTileTypeProps { type FactoryProps = Omit; type Factory = (ref: Optional>, props: X) => JSX.Element; -type FactoryMap = Record; const MessageEventFactory: Factory = (ref, props) => ; const KeyVerificationConclFactory: Factory = (ref, props) => ; @@ -90,40 +89,40 @@ const HiddenEventFactory: Factory = (ref, props) => ; export const JSONEventFactory: Factory = (ref, props) => ; -const EVENT_TILE_TYPES: FactoryMap = { - [EventType.RoomMessage]: MessageEventFactory, // note that verification requests are handled in pickFactory() - [EventType.Sticker]: MessageEventFactory, - [M_POLL_START.name]: MessageEventFactory, - [M_POLL_START.altName]: MessageEventFactory, - [EventType.KeyVerificationCancel]: KeyVerificationConclFactory, - [EventType.KeyVerificationDone]: KeyVerificationConclFactory, - [EventType.CallInvite]: CallEventFactory, // note that this requires a special factory type -}; - -const STATE_EVENT_TILE_TYPES: FactoryMap = { - [EventType.RoomEncryption]: (ref, props) => , - [EventType.RoomCanonicalAlias]: TextualEventFactory, - [EventType.RoomCreate]: (ref, props) => , - [EventType.RoomMember]: TextualEventFactory, - [EventType.RoomName]: TextualEventFactory, - [EventType.RoomAvatar]: (ref, props) => , - [EventType.RoomThirdPartyInvite]: TextualEventFactory, - [EventType.RoomHistoryVisibility]: TextualEventFactory, - [EventType.RoomTopic]: TextualEventFactory, - [EventType.RoomPowerLevels]: TextualEventFactory, - [EventType.RoomPinnedEvents]: TextualEventFactory, - [EventType.RoomServerAcl]: TextualEventFactory, +const EVENT_TILE_TYPES = new Map([ + [EventType.RoomMessage, MessageEventFactory], // note that verification requests are handled in pickFactory() + [EventType.Sticker, MessageEventFactory], + [M_POLL_START.name, MessageEventFactory], + [M_POLL_START.altName, MessageEventFactory], + [EventType.KeyVerificationCancel, KeyVerificationConclFactory], + [EventType.KeyVerificationDone, KeyVerificationConclFactory], + [EventType.CallInvite, CallEventFactory], // note that this requires a special factory type +]); + +const STATE_EVENT_TILE_TYPES = new Map([ + [EventType.RoomEncryption, (ref, props) => ], + [EventType.RoomCanonicalAlias, TextualEventFactory], + [EventType.RoomCreate, (ref, props) => ], + [EventType.RoomMember, TextualEventFactory], + [EventType.RoomName, TextualEventFactory], + [EventType.RoomAvatar, (ref, props) => ], + [EventType.RoomThirdPartyInvite, TextualEventFactory], + [EventType.RoomHistoryVisibility, TextualEventFactory], + [EventType.RoomTopic, TextualEventFactory], + [EventType.RoomPowerLevels, TextualEventFactory], + [EventType.RoomPinnedEvents, TextualEventFactory], + [EventType.RoomServerAcl, TextualEventFactory], // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - 'im.vector.modular.widgets': TextualEventFactory, // note that Jitsi widgets are special in pickFactory() - [WIDGET_LAYOUT_EVENT_TYPE]: TextualEventFactory, - [EventType.RoomTombstone]: TextualEventFactory, - [EventType.RoomJoinRules]: TextualEventFactory, - [EventType.RoomGuestAccess]: TextualEventFactory, -}; + ['im.vector.modular.widgets', TextualEventFactory], // note that Jitsi widgets are special in pickFactory() + [WIDGET_LAYOUT_EVENT_TYPE, TextualEventFactory], + [EventType.RoomTombstone, TextualEventFactory], + [EventType.RoomJoinRules, TextualEventFactory], + [EventType.RoomGuestAccess, TextualEventFactory], +]); // Add all the Mjolnir stuff to the renderer too for (const evType of ALL_RULE_TYPES) { - STATE_EVENT_TILE_TYPES[evType] = TextualEventFactory; + STATE_EVENT_TILE_TYPES.set(evType, TextualEventFactory); } // These events should be recorded in the STATE_EVENT_TILE_TYPES @@ -233,7 +232,11 @@ export function pickFactory( return noEventFactoryFactory(); // improper event type to render } - return STATE_EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory(); + if (STATE_EVENT_TILE_TYPES.get(evType) === TextualEventFactory && !hasText(mxEvent, showHiddenEvents)) { + return noEventFactoryFactory(); + } + + return STATE_EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory(); } // Blanket override for all events. The MessageEvent component handles redacted states for us. @@ -245,7 +248,7 @@ export function pickFactory( return noEventFactoryFactory(); } - return EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory(); + return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory(); } /** @@ -425,7 +428,7 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo if (!handler) return false; if (handler === TextualEventFactory) { return hasText(mxEvent, showHiddenEvents); - } else if (handler === STATE_EVENT_TILE_TYPES[EventType.RoomCreate]) { + } else if (handler === STATE_EVENT_TILE_TYPES.get(EventType.RoomCreate)) { return Boolean(mxEvent.getContent()['predecessor']); } else if (handler === JSONEventFactory) { return false; diff --git a/src/hooks/useSmoothAnimation.ts b/src/hooks/useSmoothAnimation.ts index 8d652f32579..743018aba8a 100644 --- a/src/hooks/useSmoothAnimation.ts +++ b/src/hooks/useSmoothAnimation.ts @@ -30,14 +30,12 @@ const debuglog = (...args: any[]) => { * Utility function to smoothly animate to a certain target value * @param initialValue Initial value to be used as initial starting point * @param targetValue Desired value to animate to (can be changed repeatedly to whatever is current at that time) - * @param duration Duration that each animation should take - * @param enabled Whether the animation should run or not + * @param duration Duration that each animation should take, specify 0 to skip animating */ export function useSmoothAnimation( initialValue: number, targetValue: number, duration: number, - enabled: boolean, ): number { const state = useRef<{ timestamp: DOMHighResTimeStamp | null, value: number }>({ timestamp: null, @@ -79,7 +77,7 @@ export function useSmoothAnimation( [currentStepSize, targetValue], ); - useAnimation(enabled, update); + useAnimation(duration > 0, update); - return currentValue; + return duration > 0 ? currentValue : targetValue; } diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index 8b1d6bcfb4f..90d1eb09c62 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -14,49 +14,97 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useState } from "react"; -import { ClientEvent, IMyDevice, Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MatrixClientPeg } from "../MatrixClientPeg"; +import { Notifier } from "../Notifier"; import DMRoomMap from "../utils/DMRoomMap"; -import { useEventEmitter } from "./useEventEmitter"; export interface UserOnboardingContext { - avatar: string | null; - myDevice: string; - devices: IMyDevice[]; - dmRooms: {[userId: string]: Room}; + hasAvatar: boolean; + hasDevices: boolean; + hasDmRooms: boolean; + hasNotificationsEnabled: boolean; } -export function useUserOnboardingContext(): UserOnboardingContext | null { - const [context, setContext] = useState(null); +const USER_ONBOARDING_CONTEXT_INTERVAL = 5000; + +/** + * Returns a persistent, non-changing reference to a function + * This function proxies all its calls to the current value of the given input callback + * + * This allows you to use the current value of e.g., a state in a callback that’s used by e.g., a useEventEmitter or + * similar hook without re-registering the hook when the state changes + * @param value changing callback + */ +function useRefOf(value: (...values: T) => R): (...values: T) => R { + const ref = useRef(value); + ref.current = value; + return useCallback( + (...values: T) => ref.current(...values), + [], + ); +} +function useUserOnboardingContextValue(defaultValue: T, callback: (cli: MatrixClient) => Promise): T { + const [value, setValue] = useState(defaultValue); const cli = MatrixClientPeg.get(); - const handler = useCallback(async () => { - const profile = await cli.getProfileInfo(cli.getUserId()); - const myDevice = cli.getDeviceId(); - const devices = await cli.getDevices(); + const handler = useRefOf(callback); - const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {}; - setContext({ - avatar: profile?.avatar_url ?? null, - myDevice, - devices: devices.devices, - dmRooms: dmRooms, - }); - }, [cli]); - - useEventEmitter(cli, ClientEvent.AccountData, handler); useEffect(() => { - const handle = setInterval(handler, 2000); - handler(); + if (value) { + return; + } + + let handle: number | null = null; + let enabled = true; + const repeater = async () => { + if (handle !== null) { + clearTimeout(handle); + handle = null; + } + setValue(await handler(cli)); + if (enabled) { + handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL); + } + }; + repeater().catch(err => logger.warn("could not update user onboarding context", err)); + cli.on(ClientEvent.AccountData, repeater); return () => { - if (handle) { - clearInterval(handle); + enabled = false; + cli.off(ClientEvent.AccountData, repeater); + if (handle !== null) { + clearTimeout(handle); + handle = null; } }; - }, [handler]); + }, [cli, handler, value]); + return value; +} + +export function useUserOnboardingContext(): UserOnboardingContext | null { + const hasAvatar = useUserOnboardingContextValue(false, async (cli) => { + const profile = await cli.getProfileInfo(cli.getUserId()); + return Boolean(profile?.avatar_url); + }); + const hasDevices = useUserOnboardingContextValue(false, async (cli) => { + const myDevice = cli.getDeviceId(); + const devices = await cli.getDevices(); + return Boolean(devices.devices.find(device => device.device_id !== myDevice)); + }); + const hasDmRooms = useUserOnboardingContextValue(false, async () => { + const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {}; + return Boolean(Object.keys(dmRooms).length); + }); + const hasNotificationsEnabled = useUserOnboardingContextValue(false, async () => { + return Notifier.isPossible(); + }); - return context; + return useMemo( + () => ({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled }), + [hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled], + ); } diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index daef154de02..65fb032d228 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -25,14 +25,15 @@ import { _t } from "../languageHandler"; import Modal from "../Modal"; import { Notifier } from "../Notifier"; import PosthogTrackers from "../PosthogTrackers"; +import SdkConfig from "../SdkConfig"; import { UseCase } from "../settings/enums/UseCase"; import { useSettingValue } from "./useSettings"; -import { UserOnboardingContext, useUserOnboardingContext } from "./useUserOnboardingContext"; +import { UserOnboardingContext } from "./useUserOnboardingContext"; export interface UserOnboardingTask { id: string; - title: string; - description: string; + title: string | (() => string); + description: string | (() => string); relevant?: UseCase[]; action?: { label: string; @@ -46,8 +47,6 @@ interface InternalUserOnboardingTask extends UserOnboardingTask { completed: (ctx: UserOnboardingContext) => boolean; } -const hasOpenDMs = (ctx: UserOnboardingContext) => Boolean(Object.entries(ctx.dmRooms).length); - const onClickStartDm = (ev: ButtonEvent) => { PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev); defaultDispatcher.dispatch({ action: 'view_create_chat' }); @@ -64,7 +63,7 @@ const tasks: InternalUserOnboardingTask[] = [ id: "find-friends", title: _t("Find and invite your friends"), description: _t("It’s what you’re here for, so lets get to it"), - completed: hasOpenDMs, + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, relevant: [UseCase.PersonalMessaging, UseCase.Skip], action: { label: _t("Find friends"), @@ -75,7 +74,7 @@ const tasks: InternalUserOnboardingTask[] = [ id: "find-coworkers", title: _t("Find and invite your co-workers"), description: _t("Get stuff done by finding your teammates"), - completed: hasOpenDMs, + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, relevant: [UseCase.WorkMessaging], action: { label: _t("Find people"), @@ -86,7 +85,7 @@ const tasks: InternalUserOnboardingTask[] = [ id: "find-community-members", title: _t("Find and invite your community members"), description: _t("Get stuff done by finding your teammates"), - completed: hasOpenDMs, + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, relevant: [UseCase.CommunityMessaging], action: { label: _t("Find people"), @@ -95,11 +94,13 @@ const tasks: InternalUserOnboardingTask[] = [ }, { id: "download-apps", - title: _t("Download Element"), - description: _t("Don’t miss a thing by taking Element with you"), - completed: (ctx: UserOnboardingContext) => { - return Boolean(ctx.devices.filter(it => it.device_id !== ctx.myDevice).length); - }, + title: () => _t("Download %(brand)s", { + brand: SdkConfig.get("brand"), + }), + description: () => _t("Don’t miss a thing by taking %(brand)s with you", { + brand: SdkConfig.get("brand"), + }), + completed: (ctx: UserOnboardingContext) => ctx.hasDevices, action: { label: _t("Download apps"), onClick: (ev: ButtonEvent) => { @@ -112,7 +113,7 @@ const tasks: InternalUserOnboardingTask[] = [ id: "setup-profile", title: _t("Set up your profile"), description: _t("Make sure people know it’s really you"), - completed: (info: UserOnboardingContext) => Boolean(info.avatar), + completed: (ctx: UserOnboardingContext) => ctx.hasAvatar, action: { label: _t("Your profile"), onClick: (ev: ButtonEvent) => { @@ -128,7 +129,7 @@ const tasks: InternalUserOnboardingTask[] = [ id: "permission-notifications", title: _t("Turn on notifications"), description: _t("Don’t miss a reply or important message"), - completed: () => Notifier.isPossible(), + completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled, action: { label: _t("Enable notifications"), onClick: (ev: ButtonEvent) => { @@ -140,13 +141,12 @@ const tasks: InternalUserOnboardingTask[] = [ }, ]; -export function useUserOnboardingTasks(): [UserOnboardingTask[], UserOnboardingTask[]] { +export function useUserOnboardingTasks(context: UserOnboardingContext): [UserOnboardingTask[], UserOnboardingTask[]] { const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; const relevantTasks = useMemo( () => tasks.filter(it => !it.relevant || it.relevant.includes(useCase)), [useCase], ); - const onboardingInfo = useUserOnboardingContext(); - const completedTasks = relevantTasks.filter(it => onboardingInfo && it.completed(onboardingInfo)); + const completedTasks = relevantTasks.filter(it => context && it.completed(context)); return [completedTasks, relevantTasks.filter(it => !completedTasks.includes(it))]; } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index e068de9d5c1..ec34a35c1c3 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3480,5 +3480,62 @@ "You made it!": "Zvládli jste to!", "Help": "Nápověda", "iOS": "iOS", - "Android": "Android" + "Android": "Android", + "We're creating a room with %(names)s": "Vytváříme místnost s %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play a logo Google Play jsou ochranné známky společnosti Google LLC.", + "App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® a logo Apple® jsou ochranné známky společnosti Apple Inc.", + "Get it on F-Droid": "Získat na F-Droid", + "Get it on Google Play": "Získat na Google Play", + "Download on the App Store": "Stáhnout v App Store", + "Download %(brand)s Desktop": "Stáhnout %(brand)s Desktop", + "Download %(brand)s": "Stáhnout %(brand)s", + "Unverified": "Neověřeno", + "Verified": "Ověřeno", + "Inactive for %(inactiveAgeDays)s+ days": "Neaktivní po dobu %(inactiveAgeDays)s+ dnů", + "Session details": "Podrobnosti o relaci", + "IP address": "IP adresa", + "Device": "Zařízení", + "Last activity": "Poslední aktivita", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte.", + "Other sessions": "Ostatní relace", + "Current session": "Aktuální relace", + "Sessions": "Relace", + "Verify or sign out from this session for best security and reliability.": "V zájmu nejvyšší bezpečnosti a spolehlivosti tuto relaci ověřte nebo se z ní odhlaste.", + "Unverified session": "Neověřená relace", + "This session is ready for secure messaging.": "Tato relace je připravena na bezpečné zasílání zpráv.", + "Verified session": "Ověřená relace", + "Your server doesn't support disabling sending read receipts.": "Váš server nepodporuje vypnutí odesílání potvrzení o přečtení.", + "Share your activity and status with others.": "Sdílejte své aktivity a stav s ostatními.", + "Presence": "Přítomnost", + "We’d appreciate any feedback on how you’re finding Element.": "Budeme vděční za jakoukoli zpětnou vazbu o tom, jak se vám Element osvědčil.", + "How are you finding Element so far?": "Jak se vám zatím Element líbí?", + "Welcome": "Vítejte", + "Show shortcut to welcome checklist above the room list": "Zobrazit zástupce na uvítací kontrolní seznam nad seznamem místností", + "Use new session manager (under active development)": "Použít nový správce relací (v aktivním vývoji)", + "Send read receipts": "Odesílat potvrzení o přečtení", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Zvažte odhlášení ze starých relací (%(inactiveAgeDays)s dní nebo starších), které již nepoužíváte", + "Inactive sessions": "Neaktivní relace", + "View all": "Zobrazit všechny", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Ověřte své relace pro bezpečné zasílání zpráv nebo se odhlaste z těch, které již nepoznáváte nebo nepoužíváte.", + "Unverified sessions": "Neověřené relace", + "Improve your account security by following these recommendations": "Zlepšete zabezpečení svého účtu dodržováním těchto doporučení", + "Security recommendations": "Bezpečnostní doporučení", + "Filter devices": "Filtrovat zařízení", + "Inactive for %(inactiveAgeDays)s days or longer": "Neaktivní po dobu %(inactiveAgeDays)s dní nebo déle", + "Inactive": "Neaktivní", + "Not ready for secure messaging": "Není připraveno na bezpečné zasílání zpráv", + "Ready for secure messaging": "Připraveno na bezpečné zasílání zpráv", + "All": "Všechny", + "No sessions found.": "Nebyly nalezeny žádné relace.", + "No inactive sessions found.": "Nebyly nalezeny žádné neaktivní relace.", + "No unverified sessions found.": "Nebyly nalezeny žádné neověřené relace.", + "No verified sessions found.": "Nebyly nalezeny žádné ověřené relace.", + "For best security, sign out from any session that you don't recognize or use anymore.": "Pro nejlepší zabezpečení se odhlaste z každé relace, kterou již nepoznáváte nebo nepoužíváte.", + "Verified sessions": "Ověřené relace", + "Toggle device details": "Přepnutí zobrazení podrobností o zařízení", + "Interactively verify by emoji": "Interaktivní ověření pomocí emoji", + "Manually verify by text": "Ruční ověření pomocí textu", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "Budeme rádi za jakoukoli zpětnou vazbu o tom, jak se vám %(brand)s osvědčil.", + "How are you finding %(brand)s so far?": "Jak se vám zatím %(brand)s osvědčil?", + "Don’t miss a thing by taking %(brand)s with you": "Vezměte si %(brand)s s sebou a nic vám neunikne" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 4cb248cb20f..22f78ad94f9 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -16,7 +16,7 @@ "Invites user with given id to current room": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein", "Changes your display nickname": "Ändert deinen Nicknamen", "Change Password": "Passwort ändern", - "Commands": "Kommandos", + "Commands": "Befehle", "Emoji": "Emojis", "Sign in": "Anmelden", "Warning!": "Warnung!", @@ -3409,5 +3409,6 @@ "Presence": "Anwesenheit", "Deactivating your account is a permanent action — be careful!": "Die Deaktivierung deines Kontos ist unwiderruflich - sei vorsichtig!", "Favourite Messages (under active development)": "Favorisierte Nachrichten (in aktiver Entwicklung)", - "Use new session manager (under active development)": "Benutze neue Sitzungsverwaltung (in aktiver Entwicklung)" + "Use new session manager (under active development)": "Benutze neue Sitzungsverwaltung (in aktiver Entwicklung)", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "Entwicklerbefehl: Verwirft die aktuell ausgehende Gruppensitzung und setzt eine neue Olm-Sitzung auf" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e601003ecb4..4a00a99540c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -899,7 +899,6 @@ "Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)", "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", "Send read receipts": "Send read receipts", - "Right-click message context menu": "Right-click message context menu", "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", "Use new session manager (under active development)": "Use new session manager (under active development)", @@ -951,6 +950,7 @@ "Order rooms by name": "Order rooms by name", "Show rooms with unread notifications first": "Show rooms with unread notifications first", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", + "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", @@ -1002,8 +1002,8 @@ "Get stuff done by finding your teammates": "Get stuff done by finding your teammates", "Find people": "Find people", "Find and invite your community members": "Find and invite your community members", - "Download Element": "Download Element", - "Don’t miss a thing by taking Element with you": "Don’t miss a thing by taking Element with you", + "Download %(brand)s": "Download %(brand)s", + "Don’t miss a thing by taking %(brand)s with you": "Don’t miss a thing by taking %(brand)s with you", "Download apps": "Download apps", "Set up your profile": "Set up your profile", "Make sure people know it’s really you": "Make sure people know it’s really you", @@ -1147,8 +1147,9 @@ "Anchor": "Anchor", "Headphones": "Headphones", "Folder": "Folder", - "How are you finding Element so far?": "How are you finding Element so far?", - "We’d appreciate any feedback on how you’re finding Element.": "We’d appreciate any feedback on how you’re finding Element.", + "Welcome": "Welcome", + "How are you finding %(brand)s so far?": "How are you finding %(brand)s so far?", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "We’d appreciate any feedback on how you’re finding %(brand)s.", "Feedback": "Feedback", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", @@ -1284,15 +1285,6 @@ "Session key:": "Session key:", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", - "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", - "Confirm signing out these devices|other": "Confirm signing out these devices", - "Confirm signing out these devices|one": "Confirm signing out this device", - "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", - "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", - "Authentication": "Authentication", "Deselect all": "Deselect all", "Select all": "Select all", "Verified devices": "Verified devices", @@ -1563,7 +1555,8 @@ "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", "Sessions": "Sessions", - "Current session": "Current session", + "Other sessions": "Other sessions", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", @@ -1692,7 +1685,49 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Current session": "Current session", + "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "Confirm signing out these devices|other": "Confirm signing out these devices", + "Confirm signing out these devices|one": "Confirm signing out this device", + "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", + "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", + "Authentication": "Authentication", + "Session ID": "Session ID", "Last activity": "Last activity", + "Device": "Device", + "IP address": "IP address", + "Session details": "Session details", + "Toggle device details": "Toggle device details", + "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", + "Verified": "Verified", + "Unverified": "Unverified", + "Verified session": "Verified session", + "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Unverified session": "Unverified session", + "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", + "Verified sessions": "Verified sessions", + "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", + "Unverified sessions": "Unverified sessions", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", + "Inactive sessions": "Inactive sessions", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", + "No verified sessions found.": "No verified sessions found.", + "No unverified sessions found.": "No unverified sessions found.", + "No inactive sessions found.": "No inactive sessions found.", + "No sessions found.": "No sessions found.", + "Show all": "Show all", + "All": "All", + "Ready for secure messaging": "Ready for secure messaging", + "Not ready for secure messaging": "Not ready for secure messaging", + "Inactive": "Inactive", + "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", + "Filter devices": "Filter devices", + "Security recommendations": "Security recommendations", + "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", + "View all": "View all", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -2212,7 +2247,6 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", - "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", @@ -2459,7 +2493,6 @@ "We don't record or profile any account data": "We don't record or profile any account data", "We don't share information with third parties": "We don't share information with third parties", "You can turn this off anytime in settings": "You can turn this off anytime in settings", - "Download %(brand)s": "Download %(brand)s", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", "Download on the App Store": "Download on the App Store", @@ -2718,7 +2751,6 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", - "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Verify session": "Verify session", @@ -2833,8 +2865,8 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", "Not Trusted": "Not Trusted", - "Manually Verify by Text": "Manually Verify by Text", - "Interactively verify by Emoji": "Interactively verify by Emoji", + "Manually verify by text": "Manually verify by text", + "Interactively verify by emoji": "Interactively verify by emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 3a228df505f..1350a6935b5 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3445,5 +3445,86 @@ "Send your first message to invite to chat": "Envía tu primer mensaje para invitar a a la conversación", "Saved Items": "Elementos guardados", "Messages in this chat will be end-to-end encrypted.": "Los mensajes en esta conversación serán cifrados de extremo a extremo.", - "Favourite Messages (under active development)": "Mensajes favoritos (en desarrollo)" + "Favourite Messages (under active development)": "Mensajes favoritos (en desarrollo)", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play y el logo de Google Play son marcas registradas de Google LLC.", + "App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® y el logo de Apple® son marcas registradas de Apple Inc.", + "Get it on F-Droid": "Disponible en F-Droid", + "Get it on Google Play": "Disponible en Google Play", + "Android": "Android", + "Download on the App Store": "Descargar en la App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Descargar %(brand)s para escritorio", + "Download %(brand)s": "Descargar %(brand)s", + "Choose a locale": "Elige un idioma", + "Help": "Ayuda", + "Unverified": "Sin verificar", + "Verified": "Verificada", + "Session details": "Detalles de la sesión", + "IP address": "Dirección IP", + "Device": "Dispositivo", + "Last activity": "Última actividad", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Para más seguridad, verifica tus sesiones y cierra cualquiera que no reconozcas o hayas dejado de usar.", + "Other sessions": "Otras sesiones", + "Current session": "Sesión actual", + "Sessions": "Sesiones", + "Unverified session": "Sesión sin verificar", + "This session is ready for secure messaging.": "Esta sesión está lista para mensajería segura.", + "Verified session": "Sesión verificada", + "Your server doesn't support disabling sending read receipts.": "Tu servidor no permite desactivar los acuses de recibo.", + "Share your activity and status with others.": "Comparte tu actividad y estado con los demás.", + "Presence": "Presencia", + "Spell check": "Corrector ortográfico", + "Complete these to get the most out of %(brand)s": "Complétalos para sacar el máximo partido a %(brand)s", + "You did it!": "¡Ya está!", + "Only %(count)s steps to go|one": "Solo queda %(count)s paso", + "Only %(count)s steps to go|other": "Quedan solo %(count)s pasos", + "Welcome to %(brand)s": "Te damos la bienvenida a %(brand)s", + "Secure messaging for friends and family": "Mensajería segura para amigos y familia", + "We’d appreciate any feedback on how you’re finding Element.": "Te agradeceríamos si nos das tu opinión sobre Element.", + "How are you finding Element so far?": "¿Qué te está pareciendo Element?", + "Enable notifications": "Activar notificaciones", + "Don’t miss a reply or important message": "No te pierdas ninguna respuesta ni mensaje importante", + "Turn on notifications": "Activar notificaciones", + "Your profile": "Tu perfil", + "Set up your profile": "Completar perfil", + "Download apps": "Descargar apps", + "Don’t miss a thing by taking Element with you": "No te pierdas nada, lleva Element contigo", + "Download Element": "Descargar Element", + "Find people": "Encontrar gente", + "Find and invite your friends": "Encuentra e invita a tus amigos", + "Use new session manager (under active development)": "Usar el nuevo gestor de sesiones (en desarrollo)", + "Send read receipts": "Enviar acuses de recibo", + "Interactively verify by emoji": "Verificar interactivamente usando emojis", + "Manually verify by text": "Verificar manualmente usando un texto", + "View all": "Ver todas", + "Improve your account security by following these recommendations": "Mejora la seguridad de tu cuenta siguiendo estas recomendaciones", + "Security recommendations": "Consejos de seguridad", + "Filter devices": "Filtrar dispositivos", + "Inactive for %(inactiveAgeDays)s days or longer": "Inactiva durante %(inactiveAgeDays)s días o más", + "Inactive": "Inactiva", + "Not ready for secure messaging": "No preparado para mensajería segura", + "Ready for secure messaging": "Mensajería segura lista", + "All": "Todo", + "No sessions found.": "No se ha encontrado ninguna sesión.", + "No inactive sessions found.": "No se ha encontrado ninguna sesión inactiva.", + "No unverified sessions found.": "No se ha encontrado ninguna sesión sin verificar.", + "No verified sessions found.": "No se ha encontrado ninguna sesión verificada.", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Considera cerrar las sesiones antiguas (usadas hace más de %(inactiveAgeDays)s)", + "Inactive sessions": "Sesiones inactivas", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verifica tus sesiones para una mensajería más segura, o cierra las que no reconozcas o hayas dejado de usar.", + "Unverified sessions": "Sesiones sin verificar", + "For best security, sign out from any session that you don't recognize or use anymore.": "Para mayor seguridad, cierra cualquier sesión que no reconozcas o que ya no uses.", + "Verify or sign out from this session for best security and reliability.": "Verifica o cierra esta sesión, para mayor seguridad y estabilidad.", + "Verified sessions": "Sesiones verificadas", + "Inactive for %(inactiveAgeDays)s+ days": "Inactivo durante más de %(inactiveAgeDays)s días", + "Toggle device details": "Mostrar u ocultar detalles del dispositivo", + "Find your people": "Encuentra a tus contactos", + "Find your co-workers": "Encuentra a tus compañeros", + "Secure messaging for work": "Mensajería segura para el trabajo", + "Start your first chat": "Empieza tu primera conversación", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "Gracias a la mensajería cifrada de extremo a extremo, y a las llamadas de voz y vídeo sin límite, %(brand)s es una buena manera de mantenerte en contacto.", + "Welcome": "Te damos la bienvenida", + "Find and invite your co-workers": "Encuentra o invita a tus compañeros", + "Find friends": "Encontrar amigos", + "You made it!": "¡Ya está!" } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index f98bc3a873f..d23c77e2c2c 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3494,5 +3494,26 @@ "Last activity": "Viimased tegevused", "Sessions": "Sessionid", "Use new session manager (under active development)": "Uus sessioonihaldur (aktiivselt arendamisel)", - "Current session": "Praegune sessioon" + "Current session": "Praegune sessioon", + "Welcome": "Tere tulemast", + "Show shortcut to welcome checklist above the room list": "Näita viidet jututubade loendi kohal", + "Inactive for %(inactiveAgeDays)s+ days": "Pole olnud kasutusel %(inactiveAgeDays)s+ päeva", + "Verify or sign out from this session for best security and reliability.": "Parima turvalisuse ja töökindluse nimel verifitseeri see sessioon või logi ta võrgust välja.", + "Unverified session": "Verifitseerimata sessioon", + "This session is ready for secure messaging.": "See sessioon on valmis turvaliseks sõnumivahetuseks.", + "Verified session": "Verifitseeritud sessioon", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta.", + "Other sessions": "Muud sessioonid", + "Session details": "Sessiooni teave", + "IP address": "IP-aadress", + "Device": "Seade", + "Unverified": "Verifitseerimata", + "Verified": "Verifitseeritud", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning logi neist välja, mida sa enam ei kasuta või ei tunne enam ära.", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Kui sa ei kasuta oma vanu sessioone (vanemad kui %(inactiveAgeDays)s päeva), siis logi need võrgust välja", + "Inactive sessions": "Mitteaktiivsed sessioonid", + "View all": "Näita kõiki", + "Unverified sessions": "Verifitseerimata sessioonid", + "Improve your account security by following these recommendations": "Kui järgid neid soovitusi, siis sa parandad oma kasutajakonto turvalisust", + "Security recommendations": "Turvalisusega seotud soovitused" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f48c7468d26..5623dea3c31 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3498,5 +3498,41 @@ "Last activity": "Dernière activité", "Current session": "Cette session", "Sessions": "Sessions", - "Use new session manager (under active development)": "Utiliser un nouveau gestionnaire de session (en cours de développement)" + "Use new session manager (under active development)": "Utiliser un nouveau gestionnaire de session (en cours de développement)", + "Interactively verify by emoji": "Vérifier de façon interactive avec des émojis", + "Manually verify by text": "Vérifier manuellement avec un texte", + "View all": "Voir tout", + "Improve your account security by following these recommendations": "Améliorez la sécurité de votre compte à l’aide de ces recommandations", + "Security recommendations": "Recommandations de sécurité", + "Filter devices": "Filtrer les appareils", + "Inactive for %(inactiveAgeDays)s days or longer": "Inactif depuis au moins %(inactiveAgeDays)s jours", + "Inactive": "Inactif", + "Not ready for secure messaging": "Pas prêt pour une messagerie sécurisée", + "Ready for secure messaging": "Prêt pour une messagerie sécurisée", + "All": "Tout", + "No sessions found.": "Aucune session n’a été trouvée.", + "No inactive sessions found.": "Aucune session inactive n’a été trouvée.", + "No unverified sessions found.": "Aucune session non vérifiée n’a été trouvée.", + "No verified sessions found.": "Aucune session vérifiée n’a été trouvée.", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Pensez à déconnectez les anciennes sessions (%(inactiveAgeDays)s jours ou plus) que vous n’utilisez plus", + "Inactive sessions": "Sessions inactives", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Vérifiez vos sessions pour améliorer la sécurité de votre messagerie, ou déconnectez celles que vous ne connaissez pas ou n’utilisez plus.", + "Unverified sessions": "Sessions non vérifiées", + "For best security, sign out from any session that you don't recognize or use anymore.": "Pour une meilleure sécurité, déconnectez toutes les sessions que vous ne connaissez pas ou que vous n’utilisez plus.", + "Verified sessions": "Sessions vérifiées", + "Verify or sign out from this session for best security and reliability.": "Vérifiez ou déconnectez cette session pour une meilleure sécurité et fiabilité.", + "Unverified session": "Session non vérifiée", + "This session is ready for secure messaging.": "Cette session est prête pour l’envoi de messages sécurisés.", + "Verified session": "Session vérifiée", + "Unverified": "Non vérifié", + "Verified": "Vérifié", + "Inactive for %(inactiveAgeDays)s+ days": "Inactif depuis plus de %(inactiveAgeDays)s jours", + "Toggle device details": "Afficher/masquer les détails de l’appareil", + "Session details": "Détails de session", + "IP address": "Adresse IP", + "Device": "Appareil", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous n’utilisez plus.", + "Other sessions": "Autres sessions", + "Welcome": "Bienvenue", + "Show shortcut to welcome checklist above the room list": "Afficher le raccourci vers la liste de vérification de bienvenue au-dessus de la liste des salons" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 1ceacdc5735..9087b86bed5 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3494,5 +3494,21 @@ "Your server doesn't support disabling sending read receipts.": "O teu servidor non ten soporte para desactivar o envío de resgardos de lectura.", "Share your activity and status with others.": "Comparte a túa actividade e estado con outras persoas.", "Presence": "Presenza", - "Send read receipts": "Enviar resgardos de lectura" + "Send read receipts": "Enviar resgardos de lectura", + "Unverified": "Non verificada", + "Verified": "Verificada", + "Inactive for %(inactiveAgeDays)s+ days": "Inactiva durante %(inactiveAgeDays)s+ días", + "Session details": "Detalles da sesión", + "IP address": "Enderezo IP", + "Device": "Dispositivo", + "Last activity": "Última actividade", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Para maior seguridade, verifica as túas sesións e pecha calquera sesión que non recoñezas como propia.", + "Other sessions": "Outras sesións", + "Current session": "Sesión actual", + "Sessions": "Sesións", + "Verify or sign out from this session for best security and reliability.": "Verifica ou pecha esta sesión para máis seguridade e fiabilidade.", + "Unverified session": "Sesión non verificada", + "This session is ready for secure messaging.": "Esta sesión está preparada para mensaxería segura.", + "Verified session": "Sesión verificada", + "Use new session manager (under active development)": "Usar novo xestor da sesión (en desenvolvemento)" } diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 450b62b9b74..ee7567b4ef4 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -28,7 +28,7 @@ "PM": "PM", "AM": "AM", "Warning": "התראה", - "Submit debug logs": "הזן יומני ניפוי שגיאה (דבאג)", + "Submit debug logs": "צרף לוגים", "Edit": "ערוך", "Online": "מקוון", "Register": "צור חשבון", @@ -60,12 +60,12 @@ "Friday": "שישי", "Update": "עדכון", "What's New": "מה חדש", - "On": "דלוק", + "On": "התראה", "Changelog": "דו\"ח שינויים", "Waiting for response from server": "ממתין לתשובה מהשרת", "Failed to send logs: ": "כשל במשלוח יומנים: ", "This Room": "החדר הזה", - "Noisy": "רועש", + "Noisy": "התרעה רועשת", "Messages containing my display name": "הודעות המכילות את שם התצוגה שלי", "Messages in one-to-one chats": "הודעות בשיחות פרטיות", "Unavailable": "לא זמין", @@ -97,7 +97,7 @@ "Call invitation": "הזמנה לשיחה", "Downloading update...": "מוריד עדכון...", "What's new?": "מה חדש?", - "When I'm invited to a room": "מתי אני מוזמן לחדר", + "When I'm invited to a room": "כאשר אני מוזמן לחדר", "Unable to look up room ID from server": "לא ניתן לאתר מזהה חדר על השרת", "Couldn't find a matching Matrix room": "לא נמצא חדר כזה ב מטריקס", "Invite to this room": "הזמן לחדר זה", @@ -113,7 +113,7 @@ "Yesterday": "אתמול", "Error encountered (%(errorDetail)s).": "ארעה שגיעה %(errorDetail)s .", "Low Priority": "עדיפות נמוכה", - "Off": "סגור", + "Off": "ללא", "%(brand)s does not know how to join a room on this network": "%(brand)s אינו יודע כיצד להצטרף לחדר ברשת זו", "Failed to remove tag %(tagName)s from room": "נכשל בעת נסיון הסרת תג %(tagName)s מהחדר", "Event Type": "סוג ארוע", @@ -875,7 +875,7 @@ "My Ban List": "רשימת החסומים שלי", "When rooms are upgraded": "כאשר חדרים משתדרגים", "Encrypted messages in group chats": "הודעות מוצפנות בצאטים של קבוצות", - "Encrypted messages in one-to-one chats": "הודעות מוצפנות בחדרים של אחד-ל-לאחד", + "Encrypted messages in one-to-one chats": "הודעות מוצפנות בחדרים של אחד-על-אחד", "Messages containing @room": "הודעות שמכילות שם חדר כגון: room@", "Messages containing my username": "הודעות שמכילות את שם המשתמש שלי", "Downloading logs": "מוריד לוגים", @@ -917,7 +917,7 @@ "Show avatar changes": "הצג שינויים באווטר", "Show a placeholder for removed messages": "הצד מקום לתצוגת הודעות שהוסרו", "Enable Emoji suggestions while typing": "החל הצעות לסמלים בזמן כתיבה", - "Use custom size": "השתמש בגודל מותאם אישית", + "Use custom size": "השתמשו בגודל מותאם אישית", "Font size": "גודל אותיות", "Show info about bridges in room settings": "הצג מידע אודות גשרים בהגדרות של החדרים", "Offline encrypted messaging using dehydrated devices": "שליחת הודעות מוצפנות במצב אופליין עם שימוש במכשיר מיובש", @@ -959,7 +959,7 @@ "Contact your server admin.": "צרו קשר עם מנהל השרת.", "Your homeserver has exceeded one of its resource limits.": "השרת שלכם חרג מאחד או יותר משאבים אשר הוקצו לו.", "Your homeserver has exceeded its user limit.": "השרת שלכם חרג ממגבלות מספר המשתמשים שלו.", - "Enable": "החל", + "Enable": "אפשר", "Enable desktop notifications": "אשרו התרעות שולחן עבודה", "Don't miss a reply": "אל תפספסו תגובה", "Later": "מאוחר יותר", @@ -1003,7 +1003,7 @@ "Continue With Encryption Disabled": "המשך כאשר ההצפנה מושבתת", "Incompatible Database": "מסד נתונים לא תואם", "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "השתמשת בעבר בגרסה חדשה יותר של %(brand)s עם הפעלה זו. כדי להשתמש בגרסה זו שוב עם הצפנה מקצה לקצה, יהיה עליך לצאת ולחזור שוב.", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "כדי להימנע מאיבוד היסטוריית הצ'אט שלך, עליך לייצא את מפתחות החדר שלך לפני שאתה מתנתק. יהיה עליך לחזור לגרסה החדשה יותר של %(brand)s כדי לעשות זאת", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "כדי להימנע מאיבוד היסטוריית הצ'אט שלכם, עליכם לייצא את מפתחות החדר שלכם לפני שאתם מתנתקים. יהיה עליכם לחזור לגרסה החדשה יותר של %(brand)s כדי לעשות זאת", "Sign out": "יציאה", "Block anyone not part of %(serverName)s from ever joining this room.": "חסום ממישהו שאינו חלק מ- %(serverName)s מלהצטרף אי פעם לחדר זה.", "Topic (optional)": "נושא (לא חובה)", @@ -1120,7 +1120,7 @@ "Information": "מידע", "Rotate Right": "סובב ימינה", "Rotate Left": "סובב שמאלה", - "collapse": "כווץ", + "collapse": "אחד", "expand": "הרחב", "Please create a new issue on GitHub so that we can investigate this bug.": "אנא צור בעיה חדשה ב- GitHub כדי שנוכל לחקור את הבאג הזה.", "No results": "אין תוצאות", @@ -1167,7 +1167,7 @@ "Encryption not enabled": "ההצפנה לא מופעלת", "Ignored attempt to disable encryption": "התעלם מהניסיון להשבית את ההצפנה", "Encryption enabled": "הצפנה הופעלה", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "ההודעות בחדר זה מוצפנות מקצה לקצה. כשאנשים מצטרפים, אתה יכול לאמת אותם בפרופיל שלהם, פשוט הקש על הדמות שלהם.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "ההודעות בחדר זה מוצפנות מקצה לקצה. כשאנשים מצטרפים, אתם יכולים לאמת אותם בפרופיל שלהם, פשוט הקשו על הדמות שלהם.", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "ההודעות כאן מוצפנות מקצה לקצה. אמת את %(displayName)s בפרופיל שלהם - הקש על הדמות שלהם.", "Verification cancelled": "אימות בוטל", "You cancelled verification.": "בטלתם את האימות.", @@ -1505,7 +1505,7 @@ "Change room avatar": "שנה אווטר של החדר", "Browse": "דפדף", "Set a new custom sound": "הגדר צליל מותאם אישי", - "Notification sound": "צלילי התרעה", + "Notification sound": "צליל התרעה", "Sounds": "צלילים", "Uploaded sound": "צלילים שהועלו", "Room Addresses": "כתובות חדרים", @@ -1586,20 +1586,20 @@ "Something went wrong. Please try again or view your console for hints.": "משהו השתבש. נסה שוב או הצג את המסוף שלך לקבלת רמזים.", "Error adding ignored user/server": "שגיאה בהוספת שרת\\משתמש שהתעלמתם ממנו", "Ignored/Blocked": "התעלם\\חסום", - "Labs": "מעבדות", + "Labs": "מעבדת הפיתוח", "Clear cache and reload": "נקה מטמון ואתחל", "Homeserver is": "שרת הבית הינו", "%(brand)s version:": "גרסאת %(brand)s:", "Versions": "גרסאות", "Keyboard Shortcuts": "קיצורי מקלדת", - "FAQ": "שאלות", + "FAQ": "שאלות נפוצות", "Help & About": "עזרה ואודות", - "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "כדי לדווח על בעיית אבטחה הקשורה למטריקס, אנא קראו את מדיניות גילוי האבטחה של Matrix.org .", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "כדי לדווח על בעיית אבטחה , אנא קראו את מדיניות גילוי האבטחה של Matrix.org .", "Bug reporting": "דיווח על תקלות ובאגים", "Chat with %(brand)s Bot": "דבר עם הבוט של %(brand)s", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "לעזרה בשימוש ב-%(brand)s לחץ על כאן או התחל צ'אט עם הבוט שלנו באמצעות הלחצן למטה.", "For help with using %(brand)s, click here.": "בשביל לעזור בקידום ושימוש ב- %(brand)s, לחצו כאן.", - "Credits": "בזכות", + "Credits": "נקודות זכות", "Legal": "חוקי", "General": "כללי", "Discovery": "מציאה", @@ -1825,7 +1825,7 @@ "Summary": "תקציר", "Service": "שֵׁרוּת", "To continue you need to accept the terms of this service.": "כדי להמשיך עליך לקבל את תנאי השירות הזה.", - "Terms of Service": "תנאי השירות", + "Terms of Service": "תנאי שימוש בשירות", "Use bots, bridges, widgets and sticker packs": "השתמש בבוטים, גשרים, ווידג'טים וחבילות מדבקות", "Be found by phone or email": "להימצא בטלפון או בדוא\"ל", "Find others by phone or email": "מצא אחרים בטלפון או בדוא\"ל", @@ -1852,7 +1852,7 @@ "Clear Storage and Sign Out": "נקה אחסון והתנתק", "Sign out and remove encryption keys?": "להתנתק ולהסיר מפתחות הצפנה?", "About homeservers": "אודות שרתי בית", - "Learn more": "למד עוד", + "Learn more": "לימדו עוד", "Use your preferred Matrix homeserver if you have one, or host your own.": "השתמש בשרת הבית המועדף על מטריקס אם יש לך כזה, או מארח משלך.", "Other homeserver": "שרת בית אחר", "Sign into your homeserver": "היכנס לשרת הבית שלך", @@ -1949,10 +1949,10 @@ "Integrations are disabled": "שילובים מושבתים", "Incoming Verification Request": "בקשת אימות נכנסת", "Waiting for partner to confirm...": "מחכה לשותף שיאשר ...", - "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "אימות מכשיר זה יסמן אותו כאמין, ומשתמשים שאימתו איתך יסמכו על מכשיר זה.", - "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.": "אמת את המכשיר הזה כדי לסמן אותו כאמין. אמון במכשיר זה מעניק לך ולמשתמשים אחרים שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", - "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "אימות משתמש זה יסמן את ההפעלה שלו כאמינה, וגם יסמן את ההפעלה שלך כאמינה להם.", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "אמת את המשתמש הזה כדי לסמן אותו כאמין. אמון במשתמשים מעניק לך שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "אימות מכשיר זה יסמן אותו כאמין, ומשתמשים שאימתו אתכם יסמכו על מכשיר זה.", + "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.": "אמתו את המכשיר הזה כדי לסמן אותו כאמין. אמון במכשיר זה מעניק לכם ולמשתמשים אחרים שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "אימות משתמש זה יסמן את ההפעלה שלו כאמינה, וגם יסמן את ההפעלה שלכם כאמינה להם.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "אמתו את המשתמש הזה כדי לסמן אותו כאמין. אמון במשתמשים מעניק לכם שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", "Send feedback": "שלח משוב", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "טיפ למקצוענים: אם אתה מפעיל באג, שלח יומני איתור באגים כדי לעזור לנו לאתר את הבעיה.", "Please view existing bugs on Github first. No match? Start a new one.": "אנא צפה תחילה ב באגים קיימים ב- Github . אין התאמה? התחל חדש .", @@ -1983,15 +1983,15 @@ "Upload a file": "לעלות קובץ", "Jump to oldest unread message": "קפיצה להודעה הוותיקה ביותר שלא נקראה", "Dismiss read marker and jump to bottom": "דחה את סמן הקריאה וקפוץ לתחתית", - "Toggle microphone mute": "החלף השתקה של מיקרופון", + "Toggle microphone mute": "הפעלת / השתקת מיקרופון", "Cancel replying to a message": "בטל מענה להודעה", "New line": "שורה חדשה", "Toggle Quote": "גרשיים", "Toggle Italics": "אותיות נטויות", "Toggle Bold": "הדגשת אותיות", - "Ctrl": "קונטרול", + "Ctrl": "CTRL", "Shift": "הזזה", - "Alt": "החלפה", + "Alt": "ALT", "Autocomplete": "השלמה אוטומטית", "Room List": "רשימת חדרים", "Calls": "שיחות", @@ -2019,7 +2019,7 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "קובץ הייצוא יהיה מוגן באמצעות משפט סיסמה. עליך להזין כאן את משפט הסיסמה כדי לפענח את הקובץ.", "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 room keys": "יבא מפתחות חדר", - "Export": "יצא", + "Export": "ייצוא", "Confirm passphrase": "אשר ביטוי", "Enter passphrase": "הזן ביטוי סיסמה", "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 passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "הקובץ המיוצא יאפשר לכל מי שיוכל לקרוא אותו לפענח את כל ההודעות המוצפנות שאתה יכול לראות, לכן עליך להקפיד לשמור עליו מאובטח. כדי לעזור בכך, עליך להזין משפט סיסמה למטה, שישמש להצפנת הנתונים המיוצאים. ניתן יהיה לייבא את הנתונים רק באמצעות אותו ביטוי סיסמה.", @@ -2205,7 +2205,7 @@ "Workspace: ": "סביבת עבודה: ", "Change which room, message, or user you're viewing": "שנה את החדר, ההודעה או המשתמש שאתה צופה בו", "Expand code blocks by default": "הרחב את בלוקי הקוד כברירת מחדל", - "Show stickers button": "הצג את לחצן הסטיקרים", + "Show stickers button": "הצג את לחצן המדבקות", "Use app": "השתמש באפליקציה", "Use app for a better experience": "השתמש באפליקציה לחוויה טובה יותר", "Converts the DM to a room": "המר את ה- DM לחדר שיחוח", @@ -2263,7 +2263,7 @@ "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "המכשיר שלך מוגדר כעת כמאומת. יש לו גישה להודעות המוצפנות שלך ומשתמשים אחרים יראו אותו כמכשיר מהימן.", "Device verified": "המכשיר אומת", "Failed to load list of rooms.": "טעינת רשימת החדרים נכשלה.", - "Failed to connect to your homeserver. Please close this dialog and try again.": "תקלת התחברות לשרת הבית. אנא סגור חלון זה ונסה שוב.", + "Failed to connect to your homeserver. Please close this dialog and try again.": "תקלת התחברות לשרת הבית. אנא סיגרו חלון זה ונסו שוב.", "Message search initialisation failed, check your settings for more information": "אתחול חיפוש ההודעות נכשל. בדוק את ההגדרות שלך למידע נוסף", "Failed to fetch your location. Please try again later.": "איתור המיקום שלך נכשל. אנא נסה שוב מאוחר יותר.", "Connection failed": "החיבור נכשל", @@ -2273,8 +2273,8 @@ "Specify a number of messages": "ציין מספר הודעות", "From the beginning": "מההתחלה", "Plain Text": "טקסט רגיל", - "Are you sure you want to exit during this export?": "האם אתה בטוח שברצונך לצאת במהלך הייצוא הזה?", - "Share your public space": "שתף את המרחב הציבורי שלך", + "Are you sure you want to exit during this export?": "האם אתם בטוחים שברצונכם לצאת במהלך הייצוא הזה?", + "Share your public space": "שתף את מרחב העבודה הציבורי שלך", "Command error: Unable to find rendering type (%(renderingType)s)": "שגיאת פקודה: לא ניתן למצוא את סוג העיבוד (%(renderingType)s)", "Command error: Unable to handle slash command.": "שגיאת פקודה: לא ניתן לטפל בפקודת לוכסן.", "You cannot place calls without a connection to the server.": "אינך יכול לבצע שיחות ללא חיבור לשרת.", @@ -2284,16 +2284,16 @@ "Don't send read receipts": "אל תשלחו אישורי קריאה", "Developer": "מפתח", "Experimental": "נִסיוֹנִי", - "Spaces": "רווחים", + "Spaces": "מרחבי עבודה", "Messaging": "הודעות", "Moderation": "מְתִינוּת", "Back to thread": "חזרה לשרשור", "Room members": "חברי החדר", "Back to chat": "חזרה לצ'אט", - "Threads": "חוטים", + "Threads": "שרשורים", "Check your devices": "בדוק את המכשירים שלך", "Sound on": "צליל דולק", - "You have unverified logins": "יש לך כניסות לא מאומתות", + "You have unverified logins": "יש לכם כניסות לא מאומתות", "Stop": "עצור", "That's fine": "זה בסדר", "Creating output...": "יוצר פלט...", @@ -2319,7 +2319,7 @@ "You have no ignored users.": "אין לך משתמשים שהתעלמו מהם.", "Displaying time": "מציג זמן", "Keyboard": "מקלדת", - "Global": "גלוֹבָּלִי", + "Global": "כללי", "Enable for this account": "הפעל עבור חשבון זה", "Loading new room": "טוען חדר חדש", "Sending invites... (%(progress)s out of %(count)s)|one": "שולח הזמנה...", @@ -2339,7 +2339,7 @@ "Share invite link": "שתף קישור להזמנה", "Creating...": "יוצר...", "Invite people": "הזמן אנשים", - "Leave Space": "השאר רווח", + "Leave Space": "עזוב את מרחב העבודה", "Access": "גישה", "Save Changes": "שמור שינוייים", "Click to copy": "לחץ להעתקה", @@ -2348,7 +2348,7 @@ "Expand": "להרחיב", "Private": "פרטי", "Public": "ציבורי", - "Create a space": "צור מרחב", + "Create a space": "צור מרחב עבודה", "Address": "כתובת", "Give feedback.": "תן משוב.", "Delete": "מחק", @@ -2385,7 +2385,7 @@ "Show current avatar and name for users in message history": "הצג שם ותמונה נוכחיים של משתמשים בהיסטוריית ההודעות", "Other rooms": "חדרים אחרים", "Silence call": "השתקת שיחה", - "You previously consented to share anonymous usage data with us. We're updating how that works.": "הסכמת בעבר לשתף איתנו מידע אנונימי לגבי השימוש שלך. אנחנו מעדכנים איך זה מתבצע.", + "You previously consented to share anonymous usage data with us. We're updating how that works.": "הסכמתם בעבר לשתף איתנו מידע אנונימי לגבי השימוש שלכם. אנחנו מעדכנים איך זה מתבצע.", "Fetching events...": "טוען אירועים...", "Creating HTML...": "מייצר HTML...", "Topic: %(topic)s": "נושא: %(topic)s", @@ -2416,7 +2416,363 @@ "Your Security Key is in your Downloads folder.": "מפתח האבטחה שלך נמצא בתיקיית ההורדות שלך.", "Confirm your Security Phrase": "אשר את ביטוי האבטחה שלך", "Secure your backup with a Security Phrase": "אבטח את הגיבוי שלך עם ביטוי אבטחה", - "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "אתה מנסה לגשת לקישור קהילה (%(groupId)s).
    קהילות כבר אינן נתמכות והוחלפו מרחבים.למידע נוסף על מרחבים עיין כאן.", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "אתם מנסים לגשת לקישור קהילה (%(groupId)s).
    קהילות כבר אינן נתמכות והוחלפו במרחבי עבודה.למידע נוסף על מרחבי עבודה עיינו כאן.", "You're already in a call with this person.": "אתה כבר בשיחה עם האדם הזה.", - "Already in call": "כבר בשיחה" + "Already in call": "כבר בשיחה", + "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sהסיר%(count)sהודעות", + "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sהסיר הודעה", + "Application window": "חלון אפליקציה", + "Results are only revealed when you end the poll": "תוצאות יהיה זמינות להצגה רק עם סגירת הסקר", + "What is your poll question or topic?": "מה השאלה או הנושא שלכם בסקר?", + "Closed poll": "סגר סקר", + "Open poll": "פתח סקר", + "Poll type": "סוג סקר", + "Sorry, the poll you tried to create was not posted.": "סליחה, הסקר שיצרתם לא פורסם.", + "Edit poll": "ערוך סקר", + "Create Poll": "צרו סקר", + "Create poll": "צרו סקר", + "Results will be visible when the poll is ended": "תוצאות יהיו זמינות כאשר הסקר יסתיים", + "Sorry, you can't edit a poll after votes have been cast.": "סליחה, אתם לא יכולים לערוך את שאלות הסקר לאחר שבוצעו הצבעות.", + "Can't edit poll": "לא ניתן לערוךסקר", + "Poll": "סקר", + "You do not have permission to start polls in this room.": "אין לכם הרשאה להתחיל סקר בחדר זה.", + "%(senderName)s has ended a poll": "%(senderName)sסיים סקר", + "%(senderName)s has started a poll - %(pollQuestion)s": "%(senderName)s התחיל סקר - %(pollQuestion)s", + "Preserve system messages": "שמור את הודעות המערכת", + "Next autocomplete suggestion": "הצעת השלמה אוטומטית הבאה", + "Previous room or DM": "חדר קודם או התכתבות ישירה", + "Next room or DM": "חדר הבא או התכתבות ישירה", + "No unverified sessions found.": "לא נמצאו הפעלות לא מאומתות.", + "Server Versions": "גירסאות שרת", + "Client Versions": "גירסאות", + "Failed to load.": "נכשל בטעינה.", + "Capabilities": "יכולות", + "Send custom state event": "שלח אירוע מצב מותאם אישית", + "<%(count)s spaces>|zero": "<מחרוזת ריקה>", + "<%(count)s spaces>|one": "<רווח>", + "Friends and family": "חברים ומשפחה", + "Android": "אנדרויד", + "An error occurred whilst sharing your live location, please try again": "אירעה שגיאה במהלך שיתוף המיקום החי שלכם, אנא נסו שוב", + "Only invited people can join.": "רק משתשים מוזמנים יכולים להצטרף.", + "Private (invite only)": "פרטי (הזמנות בלבד)", + "%(count)s Members|one": "%(count)s חברים", + "%(count)s rooms|other": "%(count)s חדרים", + "Based on %(count)s votes|one": "מתבסס על %(count)s הצבעות", + "Based on %(count)s votes|other": "מתבסס על %(count)s הצבעות", + "%(count)s votes cast. Vote to see the results|one": "%(count)s.קולות הצביעו כדי לראות את התוצאות", + "Create a video room": "צרו חדר וידאו", + "Verification requested": "התבקש אימות", + "Verify this device by completing one of the following:": "אמתו מכשיר זה על ידי מילוי אחת מהפעולות הבאות:", + "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.": "לוגים מכילים נתוני שימוש באפליקציה, לרבות שם המשתמש שלכם, המזהים או הכינויים של החדרים שבהם ביקרתם, עם אילו רכיבי ממשק משתמש ביצעתם אינטראקציה אחרונה ושמות המשתמש של משתמשים אחרים. הם אינם מכילים הודעות.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "אם שלחתם באג דרך GitHub, שליחת לוגים יכולה לעזור לנו לאתר את הבעיה. ", + "Your server doesn't support disabling sending read receipts.": "השרת שלכם לא תומך בביטול שליחת אישורי קריאה.", + "Share your activity and status with others.": "שתפו את הפעילות והסטטוס שלכם עם אחרים.", + "Presence": "נוכחות", + "Room visibility": "נראות של החדר", + "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)sשלח הודעה חבויה", + "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)sשלח%(count)sהודעות מוחבאות", + "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)sשלחו הודעות מוחבאות", + "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)sשלחו%(count)sהודעות מוחבאות", + "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sהסיר הודעה", + "Send your first message to invite to chat": "שילחו את ההודעה הראשונה שלכם להזמין את לצ'אט", + "sends hearts": "שולח לבבות", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "פקודת מפתחים: מסלקת את הפגישה הנוכחית של הקבוצה היוצאת ומגדירה הפעלות חדשות של Olm", + "User Directory": "ספריית משתמשים", + "Space Autocomplete": "השלמה אוטומטית של חלל העבודה", + "Recommended for public spaces.": "מומלץ למרחבי עבודה ציבוריים.", + "Allow people to preview your space before they join.": "אפשרו לאנשים תצוגה מקדימה של מרחב העבודה שלכם לפני שהם מצטרפים.", + "Preview Space": "תצוגה מקדימה של מרחב העבודה", + "Failed to update the visibility of this space": "עדכון הנראות של מרחב העבודה הזה נכשל", + "Size Limit": "הגבלת גודל", + "Format": "פורמט", + "Exporting your data": "מייצא את המידע שלכם", + "Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "האם אתם בטוחים שברצונכם להפסיק לייצא את הנתונים שלכם? אם כן, תצטרכו להתחיל מחדש.", + "Your export was successful. Find it in your Downloads folder.": "הייצוא שלכם הצליח. מיצאו אותו בתיקיית ההורדות שלכם.", + "Export Successful": "ייצוא בוצע בהצלחה", + "The export was cancelled successfully": "הייצוא בוטל בהצלחה", + "Export Cancelled": "ייצוא בוטל", + "MB": "MB", + "Number of messages": "מספר הודעות", + "Number of messages can only be a number between %(min)s and %(max)s": "מספר ההודעות יכול להיות רק מספר בין %(min)s ו %(max)s", + "Size can only be a number between %(min)s MB and %(max)s MB": "גודל יכול להיות רק מספר בין MB %(min)s ו MB%(max)s", + "Enter a number between %(min)s and %(max)s": "הכניסו מספר בין %(min)s ל %(max)s", + "Processing...": "מעבד...", + "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 Poll": "סיים סקר", + "Sorry, the poll did not end. Please try again.": "סליחה, הסקר לא הסתיים. נא נסו שוב.", + "The poll has ended. Top answer: %(topAnswer)s": "הסקר הסתיים. תשובה הכי נפוצה: %(topAnswer)s", + "The poll has ended. No votes were cast.": "הסקר הסתיים. לא היו הצבעות.", + "Room ID: %(roomId)s": "זיהוי חדר: %(roomId)s", + "Welcome to ": "ברוכים הבאים אל ", + "Search names and descriptions": "חיפוש שמות ותיאורים", + "Rooms and spaces": "חדרים וחללי עבודה", + "Results": "תוצאות", + "You may want to try a different search or check for typos.": "אולי תרצו לנסות חיפוש אחר או לבדוק אם יש שגיאות הקלדה.", + "Your server does not support showing space hierarchies.": "השרת שלכם אינו תומך בהצגת היררכית חללי עבודה.", + "We're creating a room with %(names)s": "יצרנו חדר עם %(names)s", + "Jump to the given date in the timeline": "קיפצו לתאריך הנתון בציר הזמן", + "Thread": "שרשורים", + "Threads are a beta feature": "שרשורים הם תכונה ניסיונית", + "Keep discussions organised with threads": "שימרו על דיונים מאורגנים בשרשורים", + "Tip: Use “%(replyInThread)s” when hovering over a message.": "טיפ: השתמש ב-\"%(replyInThread)s\" כשאתם מרחפים מעל הודעה.", + "Threads help keep your conversations on-topic and easy to track.": "שרשורים עוזרים לשמור על השיחות שלכם בנושא וקל למעקב.", + "Show all threads": "הצג את כל השרשורים", + "Shows all threads you've participated in": "הצג את כל השרשורים שאתם משתתפים בהם", + "Thread options": "אפשרויות שרשור", + "Collapse reply thread": "אחד שרשור של התשובות", + "Can't create a thread from an event with an existing relation": "לא ניתן ליצור שרשור מאירוע עם קשר קיים", + "Open thread": "פתיחת שרשור", + "Reply to encrypted thread…": "מענה לשרשור מוצפן…", + "From a thread": "משרשור", + "Do you want to enable threads anyway?": "האם ברצונכם לאפשר שרשורים בכל זאת ?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "השרת שלכם אינו תומך כעת בשרשורים, כך שתכונה זו עשויה להיות לא אמינה. ייתכן שהודעות שרשור מסוימות לא יהיו זמינות באופן מהימן. למידע נוסף.", + "Partial Support for Threads": "תמיכה חלקית בשרשורים", + "Reply in thread": "מענה בשרשור", + "Use “%(replyInThread)s” when hovering over a message.": "השתמשו ב-\"%(replyInThread)s\" כשאתם מרחפים מעל הודעה.", + "How can I start a thread?": "כיצד אנחנו יכולים להתחיל שרשור?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "שרשורים עוזרים לשמור על שיחות בנושא וקל למעקב. למידע נוסף.", + "Keep discussions organised with threads.": "קבצו את כל התכתובות לשרשור אחד.", + "Threaded messaging": "הודעות מקושרות", + "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "השיבו לשרשור מתמשך או השתמשו ב-\"%(replyInThread)s\" כשאתם מרחפים מעל הודעה כדי להתחיל הודעה חדשה.", + "Show:": "הצג:", + "My threads": "הקישורים שלי", + "Shows all threads from current room": "הצג את כל הקישורים מחדר זה", + "All threads": "כל הקישורים", + "We'll create rooms for each of them.": "ניצור חדרים לכל אחד מהם.", + "What projects are your team working on?": "על אילו פרויקטים הצוות שלכם עובד?", + "You can add more later too, including already existing ones.": "אתם יכולים להוסיף עוד מאוחר יותר, כולל אלה שכבר קיימים.", + "Let's create a room for each of them.": "בואו ניצור חדר לכל אחד מהם.", + "What are some things you want to discuss in %(spaceName)s?": "באילו דברים אתם רוצים לדון ב-%(spaceName)s?", + "Invite by username": "הזמנה באמצעות שם משתמש", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "זוהי תכונה ניסיונית. לעת עתה, משתמשים חדשים שמקבלים הזמנה יצטרכו לפתוח את ההזמנה ב- כדי להצטרף בפועל.", + "Make sure the right people have access. You can invite more later.": "ודאו שלאנשים הנכונים תהיה גישה. תוכלו להזמין עוד מאוחר יותר.", + "Invite your teammates": "הזמינו את חברי הצוות שלכם", + "Inviting...": "מזמין ...", + "Failed to invite the following users to your space: %(csvUsers)s": "נכשל בהזמנת המשתמשים הבאים לחלל העבודה שלכם %(csvUsers)s", + "Me and my teammates": "אני וחברי הצוות שלי", + "A private space for you and your teammates": "חלל עבודה פרטי לכם ולחברי הצוות שלכם", + "A private space to organise your rooms": "חלל עבודה פרטי לארגן בו את החדרים שלכם", + "Just me": "רק אני", + "Make sure the right people have access to %(name)s": "שימו לב שלאנשים המתאימים יש גישה אל %(name)s", + "Who are you working with?": "עם מי אתם עובדים ?", + "Go to my space": "גש לחלל העבודה שלי", + "Go to my first room": "גש לחדר הראשון שלי", + "It's just you at the moment, it will be even better with others.": "זה רק אתם כרגע, זה יהיה אפילו טוב יותר עם אחרים.", + "Share %(name)s": "שתפו %(name)s", + "Search for rooms or spaces": "חפשו חדרים או חללי עבודה", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "ביחרו חדרים או שיחות להוספה. זה רק מקום בשבילכם, אף אחד לא ייודע. תוכלו להוסיף עוד מאוחר יותר.", + "What do you want to organise?": "מה ברצונכם לארגן ?", + "Creating rooms...": "יוצר חדרים ...", + "Skip for now": "דלגו לעת עתה", + "Failed to create initial space rooms": "יצירת חדר חלל עבודה ראשוני נכשלה", + "Room name": "שם חדר", + "Support": "תמיכה", + "Random": "אקראי", + "Verify this device": "אמתו את מכשיר זה", + "Unable to verify this device": "לא ניתן לאמת את מכשיר זה", + "Jump to last message": "קיפצו להודעה האחרונה", + "Jump to first message": "קיפצו להודעה הראשונה", + "Use new session manager (under active development)": "השתמש במנהל הפעלות חדש (בפיתוח פעיל)", + "Favourite Messages (under active development)": "הודעות מועדפות (בפיתוח פעיל)", + "Live Location Sharing (temporary implementation: locations persist in room history)": "שיתוף מיקום חי (יישום זמני: המיקומים נמשכים בהיסטוריית החדרים)", + "Send read receipts": "שילחו אישורי קריאה", + "Jump to date (adds /jumptodate and jump to date headers)": "קיפצו לתאריך (מוסיף /jumptodate וקפוץ לכותרות תאריך)", + "Messages in this chat will be end-to-end encrypted.": "הודעות בצ'אט זה יוצפו מקצה לקצה.", + "Some encryption parameters have been changed.": "מספר פרמטרים של הצפנה שונו.", + "Decrypting": "מפענח", + "Downloading": "מוריד", + "Jump to date": "קיפצו לתאריך", + "Review to ensure your account is safe": "בידקו כדי לוודא שהחשבון שלך בטוח", + "Help improve %(analyticsOwner)s": "עזרו בשיפור %(analyticsOwner)s", + "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More": "שתף נתונים אנונימיים כדי לעזור לנו לזהות בעיות. ללא אישי. אין צדדים שלישיים. למידע נוסף", + "Exported %(count)s events in %(seconds)s seconds|one": "ייצא %(count)s תוך %(seconds)s שניות", + "Exported %(count)s events in %(seconds)s seconds|other": "ייצא %(count)s אירועים תוך %(seconds)s שניות", + "Fetched %(count)s events in %(seconds)ss|one": "משך %(count)s אירועים תוך %(seconds)s שניות", + "Fetched %(count)s events in %(seconds)ss|other": "עיבד %(count)s אירועים תוך %(seconds)s שניות", + "Processing event %(number)s out of %(total)s": "מעבד אירוע %(number)s מתוך %(total)s", + "This is the start of export of . Exported by at %(exportDate)s.": "זאת התחלת ייצוא של . ייצוא ע\"י ב %(exportDate)s.", + "Media omitted - file size limit exceeded": "מדיה הושמטה - גודל קובץ חרג מהמותר", + "Media omitted": "מדיה הושמטה", + "JSON": "JSON", + "HTML": "HTML", + "Fetched %(count)s events so far|one": "נטענו %(count)s אירועים עד כה", + "Fetched %(count)s events out of %(total)s|one": "טוען %(count)s אירועים מתוך %(total)s", + "Zoom out": "התמקדות החוצה", + "Zoom in": "התמקדות פנימה", + "Reset bearing to north": "נעלו את המפה לכיוון צפון", + "Mapbox logo": "לוגו", + "Location not available": "מיקום אינו זמין", + "Find my location": "מצא את מיקומי", + "Exit fullscreen": "יציאה ממסך מלא", + "Enter fullscreen": "עברו למסך מלא", + "Map feedback": "משוב על המפות", + "Toggle attribution": "דפדפו בין האפשרויות", + "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "שרת בית זה אינו מוגדר כהלכה להצגת מפות, או ששרת המפות המוגדר אינו ניתן לגישה.", + "Upgrade to %(hostSignupBrand)s": "שדרוג ל %(hostSignupBrand)s", + "Minimise dialog": "דיאלוג מינימאלי", + "Maximise dialog": "דיאלוג מקסימאלי", + "%(hostSignupBrand)s Setup": "הגדרת %(hostSignupBrand)s", + "Privacy Policy": "מדיניות פרטיות", + "Cookie Policy": "מדיניות קובצי Cookie", + "Learn more in our , and .": "קיראו עוד ב, ו.", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "המשך מאפשר זמנית לתהליך ההגדרה של %(hostSignupBrand)s לגשת לחשבון שלכם כדי להביא כתובות דוא\"ל מאומתות. נתונים אלה אינם מאוחסנים.", + "Abort": "ביטול", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "האם אתם בטוחים שברצונכם לבטל את ההגדרה? התהליך לא יוכל להמשיך.", + "Confirm abort of host creation": "אשרו ביטול הגדרה", + "You may contact me if you have any follow up questions": "אתם יכולים לתקשר איתי אם יש לכם שאלות המשך", + "Feedback sent! Thanks, we appreciate it!": "משוב נשלח! תודה, אנחנו מודים לכם", + "Search for rooms or people": "חפשו אנשים או חדרים", + "Message preview": "צפו בהודעה", + "Forward message": "העבירו את ההודעה", + "You should know": "עליכם לדעת", + "Published addresses can be used by anyone on any server to join your room.": "כל אחד בכל שרת יכול להשתמש בכתובות שפורסמו כדי להצטרף לחלל העבודה שלכם.", + "Published addresses can be used by anyone on any server to join your space.": "כל אחד בכל שרת יכול להשתמש בכתובות שפורסמו כדי להצטרף למרחב העבודה שלכם.", + "Include Attachments": "כלול קבצים מצורפים", + "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.": "אם ברצונכם לשמור על גישה להיסטוריית הצ'אט שלכם בחדרים מוצפנים, הגדירו גיבוי מפתחות או ייצאו את מפתחות ההודעות שלכם מאחד מהמכשירים האחרים שלכם לפני שתמשיך.", + "Select from the options below to export chats from your timeline": "ביחרו מבין האפשרויות למטה כדי לייצא צ'אטים מציר הזמן שלכם", + "Export Chat": "ייצוא צ'אט", + "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.": "אם אתם רוצים לשמור על גישה להיסטוריית הצ'אט שלכם בחדרים מוצפנים, עליכם לייצא תחילה את מפתחות החדר שלכם ולייבא אותם מחדש לאחר מכן.", + "Export chat": "ייצוא צ'אט", + "Joining the beta will reload %(brand)s.": "הצטרפות לפיתוח תטען מחדש את %(brand)s.", + "Leaving the beta will reload %(brand)s.": "עזיבת הניסוי תטען מחדש את %(brand)s.", + "Join the beta": "הצטרך לניסוי", + "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.": "שימו לב: זוהי תכונת פיתוח המשתמשת ביישום זמני. משמעות הדבר היא שלא תוכלו למחוק את היסטוריית המיקומים שלכם, ומשתמשים מתקדמים יוכלו לראות את היסטוריית המיקומים שלך גם לאחר שתפסיקו לשתף את המיקום החי שלכם עם החדר הזה.", + "Show Labs settings": "הצג את אופציית מעבדת הפיתוח", + "To join, please enable video rooms in Labs first": "כדי להצטרף, נא אפשר תחילה וידאו במעבדת הפיתוח", + "To view, please enable video rooms in Labs first": "כדי לצפות, אנא הפעל תחילה חדרי וידאו במעבדת הפיתוח", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "מרגישים ניסיוניים? מעבדת הפיתוח היא הדרך הטובה ביותר לנסות פיתוחים חדשים לפני כולם, לבחון תכונות חדשות ולעזור לעצב אותן לפני שהן מושקות בפועל למידע נוסף.", + "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "נהל את המכשירים המחוברים שלך . שם מכשיר גלוי לאנשים שאיתם אתה מתקשר.", + "Group all your rooms that aren't part of a space in one place.": "קבצו את כל החדרים שלכם שאינם משויכים למרחב עבודה במקום אחד.", + "Rooms outside of a space": "חדרים שמחוץ למרחב העבודה", + "Group all your people in one place.": "קבצו את כל אנשי הקשר שלכם במקום אחד.", + "Group all your favourite rooms and people in one place.": "קבצו את כל החדרים ואנשי הקשר האהובים עליכם במקום אחד.", + "Show all your rooms in Home, even if they're in a space.": "הצג את כל החדרים שלכם במסך הבית, אפילו אם הם משויכים למרחב עבודה.", + "Home is useful for getting an overview of everything.": "מסך הבית עוזר לסקירה כללית.", + "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "מרחבי עבודה הם דרך לקבץ חדרים ואנשים. במקביל למרחבי העבודה בהם אתם נמצאים ניתן להשתמש גם בכאלה שנבנו מראש.", + "Spaces to show": "מרחבי עבודה להצגה", + "Toggle webcam on/off": "הפעלת / כיבוי מצלמה", + "Send a sticker": "שלח מדבקה", + "%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s שלח מדבקה", + "Navigate to previous message in composer history": "עבור להודעה הקודמת בהיסטוריית התכתבות", + "Navigate to next message in composer history": "עבור להודעה הבאה בהיסטוריית התכתבות", + "Navigate to previous message to edit": "עבור לעריכת ההודעה הקודמת", + "Navigate to next message to edit": "עבור לעריכת ההודעה הבאה", + "Jump to end of the composer": "עבור לסוף ההתכתבות", + "Jump to start of the composer": "עבור לתחילת ההתכתבות", + "Redo edit": "חזור על העריכה", + "Undo edit": "בטל את העריכה", + "Show join/leave messages (invites/removes/bans unaffected)": "הצג הודעות הצטרפות/עזיבה (הזמנות/הסרות/איסורים) לא מושפעים", + "Images, GIFs and videos": "תמונות, GIF ווידאו", + "Code blocks": "מקטעי קוד", + "Show polls button": "הצג את כפתור הסקרים", + "Insert a trailing colon after user mentions at the start of a message": "הוסף נקודתיים לאחר אזכור המשתמש בתחילת ההודעה", + "Surround selected text when typing special characters": "סמן טקסט כאשר מקלידים סמלים מיוחדים", + "To view all keyboard shortcuts, click here.": "כדי לצפות בכל קיצורי המקלדת , ליחצו כאן.", + "All rooms you're in will appear in Home.": "כל החדרים שבהם אתם נמצאים יופיעו בדף הבית.", + "Messages containing keywords": "הודעות המכילות מילות מפתח", + "Access your secure message history and set up secure messaging by entering your Security Phrase.": "גש להיסטוריית ההודעות המאובטחת שלך והגדר הודעות מאובטחות על ידי הזנת ביטוי האבטחה שלך.", + "Doesn't look like valid JSON.": "תבנית JSON לא חוקית", + "Server": "שרת", + "Value:": "ערך:", + "Phase": "שלב", + "Forward": "קדימה", + "@mentions & keywords": "אזכורים ומילות מפתח", + "Mentions & keywords": "אזכורים ומילות מפתח", + "Failed to invite users to %(roomName)s": "נכשל בהזמנת משתמשים לחדר - %(roomName)", + "Image size in the timeline": "גודל תמונה בציר הזמן", + "Anyone will be able to find and join this space, not just members of .": "כל אחד יוכל למצוא ולהצטרך אל חלל עבודה זה. לא רק חברי .", + "Anyone in will be able to find and join.": "כל אחד ב יוכל למצוא ולהצטרף.", + "Visible to space members": "נראה לחברי מרחב העבודה", + "Anyone will be able to find and join this room, not just members of .": "כל אחד יוכל למצוא ולהצטרך אל חדר זה, לא רק משתתפי מרחב עבודה .", + "Everyone in will be able to find and join this room.": "כל אחד ב יוכל למצוא ולהצטרף אל חדר זה.", + "Adding spaces has moved.": "הוספת מרחבי עבודה הוזז.", + "Search for spaces": "חיפוש מרחבי עבודה", + "Create a new space": "הגדרת מרחב עבודה חדש", + "Want to add a new space instead?": "רוצים להוסיף מרחב עבודה חדש במקום?", + "Add existing space": "הוסף מרחב עבודה קיים", + "Backspace": "מקש חזרה לאחור", + "Ban from space": "חסום ממרחב העבודה", + "Unban from space": "הסר חסימה ממרחב העבודה", + "Remove from space": "הסר ממרחב העבודה", + "Disinvite from 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.": "לא תוכלו לבטל את השינוי הזה מכיוון שאתם מורידים לעצמכם את רמת ההרשאה, יהיה בלתי אפשרי להחזיר את ההרשאות אם אתם המשתמשים האחרונים בעלי רמת הרשאה זו במרחב עבודה זה .", + "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "הגדר כתובות עבור מרחב העבודה הזה כדי שמשתמשים יוכלו למצוא את מרחב העבודה הזה דרך השרת שלך (%(localDomain)s)", + "This space has no local addresses": "למרחב עבודה זה לא מוגדרת כתובת מקומית בשרת", + "%(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.": "%(errcode)s הוחזר בעת ניסיון לגשת לחדר או למרחב העבודה. אם אתם חושבים שאתם רואים הודעה זו בטעות, אנא שילחו דוח באג.", + "Try again later, or ask a room or space admin to check if you have access.": "נסו שנית מאוחר יותר, בקשו ממנהל החדר או מרחב העבודה לוודא אם יש לכם גישה.", + "This room or space is not accessible at this time.": "חדר זה או מרחב העבודה אינם זמינים כעת.", + "This room or space does not exist.": "חדר זה או מרחב עבודה אינם קיימים.", + "Forget this space": "שכח את מרחב עבודה זה", + "Joining space …": "מצטרף למרחב עבודה…", + "%(spaceName)s menu": "תפריט %(spaceName)s", + "You do not have permissions to add spaces to this space": "אין לכם הרשאה להוסיף מרחב עבודה אל מרחב העבודה הנוכחי", + "Add space": "הוסיפו מרחב עבודה", + "You do not have permissions to create new rooms in this space": "אין לכם הרשאה ליצור חדרים חדשים במרחב העבודה הנוכחי", + "You do not have permissions to add rooms to this space": "אין לכם השאה להוסיף חדשרים למרחב העבודה הנוכחי", + "You do not have permissions to invite people to this space": "אין לכם הרשאה להזמין משתתפים אל מרחב עבודה זה", + "Invite to space": "הזמינו אל מרחב העבודה", + "Private space": "מרחב עבודה פרטי", + "Public space": "מרחב עבודה ציבורי", + "Invite to this space": "הזמינו למרחב עבודה זה", + "Select the roles required to change various parts of the space": "ביחרו את ההרשאות הנדרשות כדי לשנות חלקים שונים של מרחב העבודה", + "Manage rooms in this space": "נהלו חדרים במרחב העבודה הנוכחי", + "Change main address for the space": "שינוי הכתובת הראשית של מרחב העבודה", + "Change space name": "שינוי שם מרחב העבודה", + "Change space avatar": "שנה את דמות מרחב העבודה", + "Space information": "מידע על מרחב העבודה", + "View older version of %(spaceName)s.": "צפו בגירסא ישנה יותר של %(spaceName)s.", + "Upgrade this space to the recommended room version": "שדרג את מרחב העבודה הזה לגרסת החדר המומלצת", + "Updating spaces... (%(progress)s out of %(count)s)|one": "מעדכן מרחב עבודה...", + "Updating spaces... (%(progress)s out of %(count)s)|other": "מעדכן את מרחבי העבודה...%(progress)s מתוך %(count)s", + "This upgrade will allow members of selected spaces access to this room without an invite.": "שדרוג זה יאפשר לחברים במרחבים נבחרים גישה לחדר זה ללא הזמנה.", + "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.": "החדר הזה נמצא בחלק ממרחבי העבודה שאתם לא מוגדרים כמנהלים בהם. במרחבים האלה, החדר הישן עדיין יוצג, אבל אנשים יתבקשו להצטרף לחדר החדש.", + "Space members": "משתתפי מרחב העבודה", + "Anyone in a space can find and join. You can select multiple spaces.": "כל אחד במרחב עבודה יכול למצוא ולהצטרף. אתם יכולים לבחור מספר מרחבי עבודה.", + "Anyone in can find and join. You can select other spaces too.": "כל אחד ב- יכול למצוא ולהצטרף. אתם יכולים לבחור גם מרחבי עבודה אחרים.", + "Spaces with access": "מרחבי עבודה עם גישה", + "Anyone in a space can find and join. Edit which spaces can access here.": "כל אחד במרחב העבודה יכול למצוא ולהצטרף. ערוך לאילו מרחבי עבודה יש גישה כאן.", + "Currently, %(count)s spaces have access|one": "כרגע, למרחב העבודה יש גישה", + "Currently, %(count)s spaces have access|other": "כרגע ל, %(count)s מרחבי עבודה יש גישה", + "Space options": "אפשרויות מרחב העבודה", + "Decide who can view and join %(spaceName)s.": "החליטו מי יכול לראות ולהצטרף אל %(spaceName)s.", + "This may be useful for public spaces.": "זה יכול להיות שימושי למרחבי עבודה ציבוריים.", + "Guests can join a space without having an account.": "אורחים יכולים להצטרף אל מרחב העבודה ללא חשבון פעיל.", + "Failed to update the history visibility of this space": "נכשל עדכון נראות ההיסטוריה של מרחב עבודה זה", + "Failed to update the guest access of this space": "עדכון גישת האורח של מרחב העבודה הזה נכשל", + "Edit settings relating to your space.": "שינוי הגדרות הנוגעות למרחב העבודה שלכם.", + "Failed to save space settings.": "כישלון בשמירת הגדרות מרחב העבודה.", + "Your private space": "מרחב העבודה הפרטי שלך", + "Your public space": "מרחב העבודה הציבורי שלך", + "To join a space you'll need an invite.": "כדי להצטרך אל מרחב עבודה, תהיו זקוקים להזמנה.", + "Open space for anyone, best for communities": "מרחב עבודה פתוח לכולם, מיועד לקהילות", + "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "מרחבי עבודה הם דרך חדשה לקבץ חדרים ואנשים. איזה סוג של מרחב עבודה אתם רוצים ליצור? תוכלו לשנות זאת מאוחר יותר.", + "e.g. my-space": "לדוגמא מרחב העבודה שלי", + "Thank you for trying Spaces. Your feedback will help inform the next versions.": "תודה שניסיתם את תכונת מרחבי העבודה. המשוב שלכם יעזור לשפר את הגרסאות הבאות.", + "Spaces feedback": "משוב על מרחבי עבודה", + "Spaces are a new feature.": "מרחבי עבודה היא תכונה חדשה.", + "Please enter a name for the space": "נא הגדירו שם עבור מרחב העבודה", + "Space selection": "בחירת מרחב עבודה", + "Explore public spaces in the new search dialog": "חיקרו מרחבי עבודה ציבוריים בתיבת הדו-שיח החדשה של החיפוש", + "The user's homeserver does not support the version of the space.": "השרת של המשתמש אינו תומך בגירסא זו של מרחבי עבודה.", + "User is already in the space": "המשתמש כבר במרחב העבודה", + "User is already invited to the space": "המשתמש כבר מוזמן למרחב העבודה", + "You do not have permission to invite people to this space.": "אין לכם הרשאה להזמין משתתפים אחרים למרחב עבודה זה.", + "In %(spaceName)s and %(count)s other spaces.|one": "ב%(spaceName)sו%(count)s מרחבי עבודה אחרים.", + "In %(spaceName)s and %(count)s other spaces.|zero": "במרחבי עבודה%(spaceName)s.", + "In %(spaceName)s and %(count)s other spaces.|other": "%(spaceName)sו%(count)s מרחבי עבודה אחרים.", + "In spaces %(space1Name)s and %(space2Name)s.": "במרחבי עבודה %(space1Name)sו%(space2Name)s.", + "Search %(spaceName)s": "חיפוש %(spaceName)s", + "sends space invaders": "שולח פולשים לחלל", + "Sends the given message with a space themed effect": "שולח את ההודעה הנתונה עם אפקט בנושא חלל", + "Invite to %(spaceName)s": "הזמן אל %(spaceName)s", + "%(spaceName)s and %(count)s others|one": "%(spaceName)sו%(count)sאחרים", + "%(spaceName)s and %(count)s others|zero": "%(spaceName)s", + "%(spaceName)s and %(count)s others|other": "%(spaceName)sו%(count)s אחרים", + "%(space1Name)s and %(space2Name)s": "%(space1Name)sו%(space2Name)s", + "To leave the beta, visit your settings.": "כדי לעזוב את התכונה הניסיונית, כנסו להגדרות.", + "Keyboard shortcuts": "קיצורי מקלדת", + "Start messages with /plain to send without markdown and /md to send with.": "התחילו הודעות עם /plain לשליחה ללא סימון ו-/md לשליחה.", + "Get notified only with mentions and keywords as set up in your settings": "קבלו התראה רק עם אזכורים ומילות מפתח כפי שהוגדרו בהגדרות שלכם", + "New keyword": "מילת מפתח חדשה", + "Keyword": "מילת מפתח" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index dec3bbc71d8..ff732f7f3dd 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -185,7 +185,7 @@ "Uploading %(filename)s and %(count)s others|zero": "%(filename)s feltöltése", "Uploading %(filename)s and %(count)s others|one": "%(filename)s és még %(count)s db másik feltöltése", "Uploading %(filename)s and %(count)s others|other": "%(filename)s és még %(count)s db másik feltöltése", - "Upload avatar": "Avatar kép feltöltése", + "Upload avatar": "Profilkép feltöltése", "Upload Failed": "Feltöltés sikertelen", "Upload new:": "Új feltöltése:", "Usage": "Használat", @@ -265,10 +265,10 @@ "Online": "Online", "Idle": "Várakozik", "Offline": "Nem érhető el", - "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s megváltoztatta a szoba avatar képét: ", - "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s törölte a szoba avatar képét.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s megváltoztatta %(roomName)s szoba avatar képét", - "Something went wrong!": "Valami tönkrement!", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s megváltoztatta a szoba profilképét: ", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s törölte a szoba profilképét.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s megváltoztatta %(roomName)s szoba profilképét", + "Something went wrong!": "Valami rosszul sikerült.", "Your browser does not support the required cryptography extensions": "A böngészője nem támogatja a szükséges titkosítási kiterjesztéseket", "Not a valid %(brand)s keyfile": "Nem érvényes %(brand)s kulcsfájl", "Authentication check failed: incorrect password?": "Hitelesítési ellenőrzés sikertelen: hibás jelszó?", @@ -315,13 +315,13 @@ "Jump to read receipt": "Olvasási visszaigazolásra ugrás", "Message Pinning": "Üzenet kitűzése", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s megváltoztatta a szoba kitűzött üzeneteit.", - "Loading...": "Betöltés...", + "Loading...": "Betöltés…", "Unnamed room": "Névtelen szoba", "And %(count)s more...|other": "És még %(count)s...", "Mention": "Megemlítés", "Invite": "Meghívás", "Delete Widget": "Kisalkalmazás törlése", - "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "A kisalkalmazás törlése minden felhasználót érint a szobában. Biztos, hogy törölni akarja?", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "A kisalkalmazás törlése minden felhasználót érint a szobában. Biztos, hogy törli a kisalkalmazást?", "Mirror local video feed": "Helyi videó folyam tükrözése", "Members only (since the point in time of selecting this option)": "Csak tagok számára (a beállítás kiválasztásától)", "Members only (since they were invited)": "Csak tagoknak (a meghívásuk idejétől)", @@ -368,10 +368,10 @@ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s megváltoztatta a nevét", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s %(count)s alkalommal megváltoztatta a nevét", "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s megváltoztatta a nevét", - "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s %(count)s alkalommal megváltoztatta az avatarját", - "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s megváltoztatta az avatarját", - "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s %(count)s alkalommal megváltoztatta az avatarját", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s megváltoztatta az avatarját", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s %(count)s alkalommal megváltoztatta a profilképét", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s megváltoztatta a profilképét", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s %(count)s alkalommal megváltoztatta a profilképét", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s megváltoztatta a profilképét", "%(items)s and %(count)s others|other": "%(items)s és még %(count)s másik", "%(items)s and %(count)s others|one": "%(items)s és még egy másik", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Az e-mail leküldésre került ide: %(emailAddress)s. Ha megnyitottad az abban lévő linket, kattints alább.", @@ -522,7 +522,7 @@ "This homeserver has hit its Monthly Active User limit.": "A Matrix-kiszolgáló elérte a havi aktív felhasználói korlátot.", "This homeserver has exceeded one of its resource limits.": "A Matrix-kiszolgáló túllépte valamelyik erőforráskorlátját.", "Upgrade Room Version": "Szoba verziójának fejlesztése", - "Create a new room with the same name, description and avatar": "Készíts egy új szobát ugyanazzal a névvel, leírással és profilképpel", + "Create a new room with the same name, description and avatar": "Készítsen egy új szobát ugyanazzal a névvel, leírással és profilképpel", "Update any local room aliases to point to the new room": "Állíts át minden helyi alternatív nevet erre a szobára", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "A felhasználóknak tiltsd meg, hogy a régi szobában beszélgessenek. Küldj egy üzenetet amiben megkéred a felhasználókat, hogy menjenek át az új szobába", "Put a link back to the old room at the start of the new room so people can see old messages": "Tegyél egy linket az új szoba elejére ami visszamutat a régi szobára, hogy az emberek lássák a régi üzeneteket", @@ -689,7 +689,7 @@ "Request media permissions": "Média jogosultságok megkérése", "Voice & Video": "Hang és videó", "Main address": "Fő cím", - "Room avatar": "Szoba képe", + "Room avatar": "Szoba profilképe", "Room Name": "Szoba neve", "Room Topic": "Szoba témája", "Join": "Belép", @@ -837,8 +837,8 @@ "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.": "Ha egyszer engedélyezve lett, a szoba titkosítását nem lehet kikapcsolni. A titkosított szobákban küldött üzenetek a kiszolgáló számára nem, csak a szoba tagjai számára láthatók. A titkosítás bekapcsolása megakadályoz sok botot és hidat a megfelelő működésben. Tudjon meg többet a titkosításról.", "Power level": "Hozzáférési szint", "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.": "Figyelmeztetés: A szoba frissítése nem fogja automatikusan átvinni a szoba résztvevőit az új verziójú szobába. A régi szobába bekerül egy link az új szobához - a tagoknak rá kell kattintani a linkre az új szobába való belépéshez.", - "Adds a custom widget by URL to the room": "Egyéni kisalkalmazás hozzáadása a szobához URL alapján", - "Please supply a https:// or http:// widget URL": "Adja meg a kisalkalmazás https:// vagy http:// URL-jét", + "Adds a custom widget by URL to the room": "Egyéni kisalkalmazás hozzáadása a szobához webcím alapján", + "Please supply a https:// or http:// widget URL": "Adja meg a kisalkalmazás https:// vagy http:// webcímét", "You cannot modify widgets in this room.": "Nem módosíthatja a kisalkalmazásokat ebben a szobában.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s visszavonta %(targetDisplayName)s a szobába való belépéséhez szükséges meghívóját.", "Upgrade this room to the recommended room version": "A szoba fejlesztése a javasolt verzióra", @@ -971,7 +971,7 @@ "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Kérlek mond el nekünk mi az ami nem működött, vagy még jobb, ha egy GitHub jegyben leírod a problémát.", "Find others by phone or email": "Keress meg másokat telefonszám vagy e-mail cím alapján", "Be found by phone or email": "Legyél megtalálható telefonszámmal vagy e-mail címmel", - "Use bots, bridges, widgets and sticker packs": "Használj botokoat, hidakat, kisalkalmazásokat és matricákat", + "Use bots, bridges, widgets and sticker packs": "Használjon botokat, hidakat, kisalkalmazásokat és matricacsomagokat", "Terms of Service": "Felhasználási feltételek", "Service": "Szolgáltatás", "Summary": "Összefoglaló", @@ -1081,7 +1081,7 @@ "%(count)s unread messages including mentions.|other": "%(count)s olvasatlan üzenet megemlítéssel.", "%(count)s unread messages.|other": "%(count)s olvasatlan üzenet.", "Show image": "Kép megjelenítése", - "Please create a new issue on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek készíts egy új hibajegyet a GitHubon.", + "Please create a new issue on GitHub so that we can investigate this bug.": "Ahhoz hogy megvizsgálhassuk a hibát, hozzon létre egy új hibajegyet a GitHubon.", "To continue you need to accept the terms of this service.": "A folytatáshoz el kell fogadnod a felhasználási feltételeket.", "Document": "Dokumentum", "Emoji Autocomplete": "Emodzsi automatikus kiegészítése", @@ -1104,8 +1104,8 @@ "This client does not support end-to-end encryption.": "A kliens nem támogatja a végponttól végpontig való titkosítást.", "Messages in this room are not end-to-end encrypted.": "Az üzenetek a szobában nincsenek végponttól végpontig titkosítva.", "Command Autocomplete": "Parancs Automatikus kiegészítés", - "Quick Reactions": "Gyors Reakció", - "Frequently Used": "Gyakran Használt", + "Quick Reactions": "Gyors reakciók", + "Frequently Used": "Gyakran használt", "Smileys & People": "Mosolyok és emberek", "Animals & Nature": "Állatok és természet", "Food & Drink": "Étel és ital", @@ -1173,14 +1173,14 @@ "Security": "Biztonság", "Verify": "Ellenőrzés", "Any of the following data may be shared:": "Az alábbi adatok közül bármelyik megosztásra kerülhet:", - "Your display name": "Megjelenítési neved", - "Your avatar URL": "Profilképed URL-je", - "Your user ID": "A felhasználói azonosítója", - "Your theme": "Témád", + "Your display name": "Saját megjelenítendő neve", + "Your avatar URL": "Saját profilképének webcíme", + "Your user ID": "Saját felhasználói azonosítója", + "Your theme": "Saját témája", "%(brand)s URL": "%(brand)s URL", - "Room ID": "Szoba azonosító", - "Widget ID": "Kisalkalmazás azonosító", - "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", + "Room ID": "Szobaazonosító", + "Widget ID": "Kisalkalmazás-azonosító", + "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s domainnel.", "Widget added by": "A kisalkalmazást hozzáadta", "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", "More options": "További beállítások", @@ -1189,7 +1189,7 @@ "Cannot connect to integration manager": "A kapcsolódás az integrációs menedzserhez sikertelen", "The integration manager is offline or it cannot reach your homeserver.": "Az integrációkezelő nem működik, vagy nem éri el a Matrix-kiszolgálóját.", "Failed to connect to integration manager": "Az integrációs menedzserhez nem sikerült csatlakozni", - "Widgets do not use message encryption.": "A kisalkalmazások nem használnak üzenet titkosítást.", + "Widgets do not use message encryption.": "A kisalkalmazások nem használnak üzenettitkosítást.", "Integrations are disabled": "Az integrációk le vannak tiltva", "Enable 'Manage Integrations' in Settings to do this.": "Ehhez engedélyezd az „Integrációk Kezelésé”-t a Beállításokban.", "Integrations not allowed": "Az integrációk nem engedélyezettek", @@ -1394,7 +1394,7 @@ "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.": "Figyelmeztetés: A személyes adataid (beleértve a titkosító kulcsokat is) továbbra is az eszközön vannak tárolva. Ha az eszközt nem használod tovább vagy másik fiókba szeretnél bejelentkezni, töröld őket.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Fejleszd ezt a munkamenetet, hogy más munkameneteket is tudj vele hitelesíteni, azért, hogy azok hozzáférhessenek a titkosított üzenetekhez és megbízhatónak legyenek jelölve más felhasználók számára.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "A másolatot tartsd biztonságos helyen, mint pl. egy jelszókezelő (vagy széf).", - "Copy": "Másol", + "Copy": "Másolás", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "A Biztonságos Üzenet Visszaállítás beállítása nélkül kijelentkezés után vagy másik munkamenetet használva nem tudod visszaállítani a titkosított üzeneteidet.", "Create key backup": "Kulcs mentés készítése", "This session is encrypting history using the new recovery method.": "Ez a munkamenet az új visszaállítási módszerrel titkosítja a régi üzeneteket.", @@ -1558,7 +1558,7 @@ "If you've joined lots of rooms, this might take a while": "Ha sok szobához csatlakozott, ez eltarthat egy darabig", "Currently indexing: %(currentRoom)s": "Indexelés alatt: %(currentRoom)s", "Send a bug report with logs": "Hibajelentés beküldése naplóval", - "Please supply a widget URL or embed code": "Adja meg a kisalkalmazás URL-jét vagy a beágyazott kódot", + "Please supply a widget URL or embed code": "Adja meg a kisalkalmazás webcímét vagy a beágyazási kódot", "Unable to query secret storage status": "A biztonsági tároló állapotát nem lehet lekérdezni", "New login. Was this you?": "Új bejelentkezés. Ön volt az?", "Restoring keys from backup": "Kulcsok visszaállítása mentésből", @@ -1610,7 +1610,7 @@ "Delete the room address %(alias)s and remove %(name)s from the directory?": "Törlöd a szoba címét: %(alias)s és eltávolítod a könyvtárból ezt: %(name)s?", "delete the address.": "cím törlése.", "Use a different passphrase?": "Másik jelmondat használata?", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "A szerver adminisztrátorod alapesetben kikapcsolta a végpontok közötti titkosítást a közvetlen beszélgetésekben.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "A kiszolgáló adminisztrátora alapértelmezetten kikapcsolta a végpontok közötti titkosítást a privát szobákban és a közvetlen beszélgetésekben.", "No recently visited rooms": "Nincsenek nemrégiben meglátogatott szobák", "People": "Felhasználók", "Sort by": "Rendezés", @@ -1676,7 +1676,7 @@ "Are you sure you want to cancel entering passphrase?": "Biztos, hogy megszakítja a jelmondat bevitelét?", "%(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.": "A(z) %(brand)s nem képes a web böngészőben futva biztonságosan elmenteni a titkosított üzeneteket helyben. Használd az Asztali %(brand)s alkalmazást ahhoz, hogy az üzenetekben való keresésekkor a titkosított üzenetek is megjelenhessenek.", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Adja meg a rendszer által használt betűkészlet nevét és az %(brand)s megpróbálja azt használni.", - "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.": "A figyelmen kívül hagyandó felhasználókat és szervereket itt add meg. %(brand)s kliensben használj csillagot hogy a helyén minden karakterre illeszkedjen a kifejezés. Például: @bot:* figyelmen kívül fog hagyni minden „bot” nevű felhasználót bármely szerverről.", + "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.": "A figyelmen kívül hagyandó felhasználókat és kiszolgálókat itt adja meg. Használjon csillagot a(z) %(brand)s kliensben, hogy minden karakterre illeszkedjen. Például a @bot:* figyelmen kívül fog hagyni minden „bot” nevű felhasználót, minden kiszolgálóról.", "Show rooms with unread messages first": "Olvasatlan üzeneteket tartalmazó szobák megjelenítése elől", "Show previews of messages": "Üzenet előnézet megjelenítése", "Edited at %(date)s": "Szerkesztve ekkor: %(date)s", @@ -1737,7 +1737,7 @@ "Secure Backup": "Biztonsági Mentés", "Start a conversation with someone using their name or username (like ).": "Indíts beszélgetést valakivel és használd hozzá a nevét vagy a felhasználói nevét (mint ).", "Invite someone using their name, username (like ) or share this room.": "Hívj meg valakit a nevét, vagy felhasználónevét (például ) megadva, vagy oszd meg ezt a szobát.", - "Add widgets, bridges & bots": "Widget-ek, hidak, és botok hozzáadása", + "Add widgets, bridges & bots": "Kisalkalmazások, hidak, és botok hozzáadása", "Your server requires encryption to be enabled in private rooms.": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.", "Unable to set up keys": "Nem sikerült a kulcsok beállítása", "Safeguard against losing access to encrypted messages & data": "Biztosíték a titkosított üzenetekhez és adatokhoz való hozzáférés elvesztése ellen", @@ -1804,7 +1804,7 @@ "Austria": "Ausztria", "Australia": "Ausztrália", "Aruba": "Aruba", - "Armenia": "Armenia", + "Armenia": "Örményország", "Argentina": "Argentína", "Antigua & Barbuda": "Antigua és Barbuda", "Antarctica": "Antarktisz", @@ -1871,7 +1871,7 @@ "Bulgaria": "Bulgária", "Brunei": "Brunei", "British Virgin Islands": "Brit Virgin-szigetek", - "British Indian Ocean Territory": "Brit Indiai-óceáni Terület", + "British Indian Ocean Territory": "Brit Indiai-óceáni terület", "Brazil": "Brazília", "Bouvet Island": "Bouvet-sziget", "Botswana": "Botswana", @@ -2030,7 +2030,7 @@ "India": "India", "Iceland": "Izland", "Hungary": "Magyarország", - "Hong Kong": "Hong Kong", + "Hong Kong": "Hongkong", "Honduras": "Honduras", "Heard & McDonald Islands": "Heard-sziget és McDonald-szigetek", "Haiti": "Haiti", @@ -2046,7 +2046,7 @@ "Greece": "Görögország", "Gibraltar": "Gibraltár", "%(creator)s created this DM.": "%(creator)s hozta létre ezt az üzenetet.", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "A szobában lévő üzenetek végpontok között titkosítottak. Miután csatlakoztak a felhasználók, ellenőrizheted őket a profiljukban, amit a profilképükre kattintással nyithatsz meg.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "A szobában lévő üzenetek végpontok között titkosítottak. Miután csatlakoztak a felhasználók, ellenőrizheti őket a profiljukban, amelyet a profilképükre kattintással nyithat meg.", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Az üzenetek végpontok között titkosítottak. Ellenőrizze %(displayName)s személyazonosságát a profilján – kattintson %(displayName)s profilképére.", "This is the start of .": "Ez a(z) kezdete.", "Add a photo, so people can easily spot your room.": "Állíts be egy fényképet, hogy az emberek könnyebben felismerjék a szobát!", @@ -2078,7 +2078,7 @@ "Decline All": "Mindet elutasít", "Approve": "Engedélyez", "This widget would like to:": "A kisalkalmazás ezeket szeretné:", - "Approve widget permissions": "Kisalkalmazás engedélyek elfogadása", + "Approve widget permissions": "Kisalkalmazás-engedélyek elfogadása", "Sign into your homeserver": "Bejelentkezés a matrix szerveredbe", "Specify a homeserver": "Matrix szerver megadása", "Invalid URL": "Érvénytelen URL", @@ -2181,7 +2181,7 @@ "Transfer": "Átadás", "Failed to transfer call": "A hívás átadása nem sikerült", "A call can only be transferred to a single user.": "Csak egy felhasználónak lehet átadni a hívást.", - "There was an error finding this widget.": "A kisalkalmazás keresésekor hiba történt.", + "There was an error finding this widget.": "Hiba történt a kisalkalmazás keresése során.", "Active Widgets": "Aktív kisalkalmazások", "Open dial pad": "Számlap megnyitása", "Dial pad": "Tárcsázó számlap", @@ -2233,7 +2233,7 @@ "Something went wrong in confirming your identity. Cancel and try again.": "A személyazonosság ellenőrzésénél valami hiba történt. Megszakítás és próbálja újra.", "Remember this": "Emlékezzen erre", "The widget will verify your user ID, but won't be able to perform actions for you:": "A kisalkalmazás ellenőrizni fogja a felhasználói azonosítóját, de az alábbi tevékenységeket nem tudja végrehajtani:", - "Allow this widget to verify your identity": "A kisalkalmazás ellenőrizheti a személyazonosságot", + "Allow this widget to verify your identity": "A kisalkalmazás ellenőrizheti a személyazonosságát", "Show stickers button": "Matrica gomb megjelenítése", "Show line numbers in code blocks": "Sorszámok megjelenítése a kódblokkokban", "Expand code blocks by default": "Kódblokk kibontása alapértelmezetten", @@ -2460,8 +2460,8 @@ "Forward message": "Üzenet továbbítása", "Sent": "Elküldve", "You don't have permission to do this": "Nincs jogosultsága ehhez", - "Error - Mixed content": "Hiba - Vegyes tartalom", - "Error loading Widget": "Kisalkalmazás betöltési hiba", + "Error - Mixed content": "Hiba – Vegyes tartalom", + "Error loading Widget": "Hiba a kisalkalmazás betöltése során", "Pinned messages": "Kitűzött üzenetek", "Nothing pinned, yet": "Még semmi sincs kitűzve", "End-to-end encryption isn't enabled": "Végpontok közötti titkosítás nincs engedélyezve", @@ -2549,11 +2549,11 @@ "Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez", "Integration manager": "Integrációs Menedzser", "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)s nem használhat Integrációs Menedzsert. Kérem vegye fel a kapcsolatot az adminisztrátorral.", - "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s oldallal és az integrációkezelőjével.", "Identity server is": "Azonosítási szerver", - "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet állíthat be az ön nevében.", - "Use an integration manager to manage bots, widgets, and sticker packs.": "Használjon Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", - "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használjon Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Az integrációkezelők megkapják a beállításokat, módosíthatják a kisalkalmazásokat, szobameghívókat küldhetnek és a hozzáférési szintet állíthatnak be az Ön nevében.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Használjon integrációkezelőt a botok, kisalkalmazások és matricacsomagok kezeléséhez.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használjon integrációkezelőt (%(serverName)s) a botok, kisalkalmazások és matricacsomagok kezeléséhez.", "Identity server": "Azonosító szerver", "Identity server (%(server)s)": "Azonosítási kiszolgáló (%(server)s)", "Could not connect to identity server": "Az Azonosítási Szerverhez nem lehet csatlakozni", @@ -2581,7 +2581,7 @@ "You can change this at any time from room settings.": "A szoba beállításokban ezt bármikor megváltoztathatja.", "Everyone in will be able to find and join this room.": " téren bárki megtalálhatja és beléphet a szobába.", "Share content": "Tartalom megosztása", - "Application window": "Alkalmazás ablak", + "Application window": "Alkalmazásablak", "Share entire screen": "A teljes képernyő megosztása", "Image": "Kép", "Sticker": "Matrica", @@ -2706,7 +2706,7 @@ "Change description": "Leírás megváltoztatása", "Change main address for the space": "Tér elsődleges címének megváltoztatása", "Change space name": "Tér nevének megváltoztatása", - "Change space avatar": "Tér profilkép megváltoztatása", + "Change space avatar": "Tér profilképének megváltoztatása", "Anyone in can find and join. You can select other spaces too.": " téren bárki megtalálhatja és beléphet. Kiválaszthat más tereket is.", "Message didn't send. Click for info.": "Az üzenet nincs elküldve. Kattintson az információkért.", "Message": "Üzenet", @@ -3021,7 +3021,7 @@ "was removed %(count)s times|other": "%(count)s alkalommal lett eltávolítva", "were removed %(count)s times|one": "eltávolítva", "were removed %(count)s times|other": "%(count)s alkalommal lett eltávolítva", - "Unknown error fetching location. Please try again later.": "A földrajzi helyzetének lekérdezésekor ismeretlen hiba történt. Kérjük próbálja meg később.", + "Unknown error fetching location. Please try again later.": "A földrajzi helyzetének lekérdezésekor ismeretlen hiba történt. Próbálja újra később.", "Timed out trying to fetch your location. Please try again later.": "A földrajzi helyzetének lekérdezésekor időtúllépés történt. Kérjük próbálja meg később.", "Failed to fetch your location. Please try again later.": "A földrajzi helyzetének lekérdezésekor hiba történt. Kérjük próbálja meg később.", "Could not fetch location": "Nem lehet elérni a földrajzi helyzetét", @@ -3146,7 +3146,7 @@ "Drop a Pin": "Hely kijelölése", "My live location": "Folyamatos földrajzi helyzetem", "My current location": "Jelenlegi földrajzi helyzetem", - "%(brand)s could not send your location. Please try again later.": "Az %(brand)s nem tudja elküldeni a földrajzi helyzetét. Kérjük, próbálja meg később.", + "%(brand)s could not send your location. Please try again later.": "Az %(brand)s nem tudja elküldeni a földrajzi helyzetét. Próbálja újra később.", "We couldn't send your location": "A földrajzi helyzetet nem sikerült elküldeni", "Insert a trailing colon after user mentions at the start of a message": "Elválasztó vessző elhelyezése egy felhasználó üzenet elején való megemlítésekor", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Válaszoljon egy meglévő szálban, vagy új szál indításához használja a „%(replyInThread)s” lehetőséget az üzenet sarkában megjelenő menüben.", @@ -3426,8 +3426,8 @@ "We'll help you get connected.": "Segítünk a kapcsolatteremtésben.", "Who will you chat to the most?": "Kivel beszélget a legtöbbet?", "You're in": "Itt vagy:", - "You need to have the right permissions in order to share locations in this room.": "A helymegosztáshoz ebben a szobában megfelelő jogosultságokra van szükséged.", - "You don't have permission to share locations": "Nincs jogosultságod a helymegosztáshoz", + "You need to have the right permissions in order to share locations in this room.": "Az ebben a szobában történő helymegosztáshoz a megfelelő jogosultságokra van szüksége.", + "You don't have permission to share locations": "Nincs jogosultsága a helymegosztáshoz", "Join the room to participate": "Csatlakozz a szobához, hogy részt vehess", "Favourite Messages (under active development)": "Kedvenc üzenetek (aktív fejlesztés alatt)", "Reset bearing to north": "Északi irányba állítás", @@ -3498,5 +3498,26 @@ "Share your activity and status with others.": "Ossza meg a tevékenységét és állapotát másokkal.", "Presence": "Állapot", "Spell check": "Helyesírás ellenőrzés", - "Complete these to get the most out of %(brand)s": "Ezen lépések befejezésével hozhatod ki a legtöbbet %(brand)s alkalmazásból" + "Complete these to get the most out of %(brand)s": "Ezen lépések befejezésével hozhatod ki a legtöbbet %(brand)s alkalmazásból", + "Unverified": "Ellenőrizetlen", + "Verified": "Ellenőrizve", + "Inactive for %(inactiveAgeDays)s+ days": "Utolsó használat %(inactiveAgeDays)s+ napja", + "Session details": "Munkamenet információk", + "IP address": "IP cím", + "Device": "Eszköz", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "A legjobb biztonság érdekében ellenőrizze a munkameneteit és jelentkezzen ki azokból amiket nem ismer fel vagy már nem használ.", + "Other sessions": "Más munkamenetek", + "Verify or sign out from this session for best security and reliability.": "A jobb biztonság vagy megbízhatóság érdekében ellenőrizze vagy jelentkezzen ki ebből a munkamenetből.", + "Unverified session": "Ellenőrizetlen munkamenet", + "This session is ready for secure messaging.": "Ez a munkamenet beállítva a biztonságos üzenetküldéshez.", + "Verified session": "Munkamenet hitelesítve", + "Welcome": "Üdv", + "Show shortcut to welcome checklist above the room list": "Kezdő lépések elvégzéséhez való hivatkozás megjelenítése a szobalista fölött", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Fontolja meg a kijelentkezést a régi munkamenetekből (%(inactiveAgeDays)s napnál régebbi) ha már nem használja azokat", + "Inactive sessions": "Nem aktív munkamenetek", + "View all": "Összes megtekintése", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Erősítse meg a munkameneteit a még biztonságosabb csevegéshez vagy jelentkezzen ki ezekből, ha nem ismeri fel vagy már nem használja őket.", + "Unverified sessions": "Meg nem erősített munkamenetek", + "Improve your account security by following these recommendations": "Javítsa a fiókja biztonságát azzal, hogy követi a következő javaslatokat", + "Security recommendations": "Biztonsági javaslatok" } diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index e4761685980..0f558b0f00a 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -3498,5 +3498,41 @@ "Last activity": "Aktivitas terakhir", "Sessions": "Sesi", "Use new session manager (under active development)": "Gunakan pengelola sesi baru (dalam pengembangan aktif)", - "Current session": "Sesi saat ini" + "Current session": "Sesi saat ini", + "Unverified": "Belum diverifikasi", + "Verified": "Terverifikasi", + "Inactive for %(inactiveAgeDays)s+ days": "Tidak aktif selama %(inactiveAgeDays)s+ hari", + "Session details": "Detail sesi", + "IP address": "Alamat IP", + "Device": "Perangkat", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Untuk keamanan yang terbaik, verifikasi sesi Anda dan keluarkan dari sesi yang Anda tidak kenal atau tidak digunakan lagi.", + "Other sessions": "Sesi lainnya", + "Verify or sign out from this session for best security and reliability.": "Verifikasi atau keluarkan dari sesi ini untuk keamanan dan keandalan yang terbaik.", + "Unverified session": "Sesi belum diverifikasi", + "This session is ready for secure messaging.": "Sesi ini siap untuk perpesanan yang aman.", + "Verified session": "Sesi terverifikasi", + "Welcome": "Selamat datang", + "Show shortcut to welcome checklist above the room list": "Tampilkan pintasan ke daftar centang selamat datang di atas daftar ruangan", + "View all": "Tampilkan semua", + "Improve your account security by following these recommendations": "Tingkatkan keamanan akun Anda dengan mengikuti saran berikut", + "Security recommendations": "Saran keamanan", + "Filter devices": "Saring perangkat", + "Inactive for %(inactiveAgeDays)s days or longer": "Tidak aktif selama %(inactiveAgeDays)s hari atau lebih", + "Inactive": "Tidak aktif", + "Not ready for secure messaging": "Belum siap untuk perpesanan aman", + "Ready for secure messaging": "Siap untuk perpesanan aman", + "All": "Semua", + "No sessions found.": "Tidak ditemukan sesi apa pun.", + "No inactive sessions found.": "Tidak ditemukan sesi yang tidak aktif.", + "No unverified sessions found.": "Tidak ditemukan sesi yang belum diverifikasi.", + "No verified sessions found.": "Tidak ditemukan sesi yang terverifikasi.", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Pertimbangkan mengeluarkan sesi lama (%(inactiveAgeDays)s hari atau lebih lama) yang Anda tidak gunakan lagi", + "Inactive sessions": "Sesi tidak aktif", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verifikasi sesi Anda untuk perpesanan aman yang baik atau keluarkan sesi yang Anda tidak kenal atau tidak digunakan lagi.", + "Unverified sessions": "Sesi belum diverifikasi", + "For best security, sign out from any session that you don't recognize or use anymore.": "Untuk keamanan yang terbaik, keluarkan sesi yang Anda tidak kenal atau tidak digunakan lagi.", + "Verified sessions": "Sesi terverifikasi", + "Toggle device details": "Saklar rincian perangkat", + "Interactively verify by emoji": "Verifikasi secara interaktif sengan emoji", + "Manually verify by text": "Verifikasi secara manual dengan teks" } diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index d7eadab7d0a..46bd60db8d2 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -446,7 +446,7 @@ "Flags": "Fánar", "Symbols": "Tákn", "Objects": "Hlutir", - "Activities": "Starfsemi", + "Activities": "Afþreying", "Document": "Skjal", "Complete": "Fullklára", "View": "Skoða", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 5ffaf3df34c..394755e717d 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3498,5 +3498,17 @@ "Share your activity and status with others.": "Condividi la tua attività e lo stato con gli altri.", "Presence": "Presenza", "Use new session manager (under active development)": "Usa il nuovo gestore di sessioni (in sviluppo attivo)", - "Send read receipts": "Invia le conferme di lettura" + "Send read receipts": "Invia le conferme di lettura", + "Unverified": "Non verificata", + "Verified": "Verificata", + "Inactive for %(inactiveAgeDays)s+ days": "Inattivo da %(inactiveAgeDays)s+ giorni", + "Session details": "Dettagli sessione", + "IP address": "Indirizzo IP", + "Device": "Dispositivo", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più.", + "Other sessions": "Altre sessioni", + "Verify or sign out from this session for best security and reliability.": "Verifica o disconnetti questa sessione per una migliore sicurezza e affidabilità.", + "Unverified session": "Sessione non verificata", + "This session is ready for secure messaging.": "Questa sessione è pronta per i messaggi sicuri.", + "Verified session": "Sessione verificata" } diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 042036debb2..c81f13d1fcc 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -429,15 +429,15 @@ "Jump to read receipt": "읽은 기록으로 건너뛰기", "Share room": "방 공유하기", "Members only (since they joined)": "구성원만(구성원들이 참여한 시점부터)", - "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s이 참여했습니다", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s님이 참여했습니다", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s이 %(count)s번 참여했습니다", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s님이 %(count)s번 참여했습니다", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s님이 참여했습니다", - "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s이 %(count)s번 참가하다가 떠났습니다", - "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s이 참가하다가 떠났습니다", - "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s님이 %(count)s번 참가하다가 떠났습니다", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s님이 참가하다가 떠났습니다", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s이 떠나고 다시 참여했습니다", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s이 %(count)s번 참여하고 떠났습니다", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s님이 참여하고 떠났습니다", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s님이 %(count)s번 참여하고 떠났습니다", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s님이 참여하고 떠났습니다", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s님이 떠나고 다시 참여했습니다", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s님이 %(count)s번 떠나고 다시 참여했습니다", "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s님이 떠나고 다시 참여했습니다", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s이 %(count)s번 떠났습니다", @@ -461,8 +461,8 @@ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s이 초대를 거절했습니다", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s님이 초대를 %(count)s번 거절했습니다", "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s님이 초대를 거절했습니다", - "were invited %(count)s times|other": "이 %(count)s번 초대받았습니다", - "were invited %(count)s times|one": "이 초대받았습니다", + "were invited %(count)s times|other": "%(count)s번 초대했습니다", + "were invited %(count)s times|one": "초대했습니다", "Event Content": "이벤트 내용", "Event Type": "이벤트 종류", "Event sent!": "이벤트를 보냈습니다!", @@ -511,7 +511,7 @@ "This room is a continuation of another conversation.": "이 방은 다른 대화방의 연장선입니다.", "Click here to see older messages.": "여길 눌러 오래된 메시지를 보세요.", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", - "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s이 %(count)s번 떠나고 다시 참여했습니다", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s님이 %(count)s번 떠나고 다시 참여했습니다", "In reply to ": "관련 대화 ", "Updating %(brand)s": "%(brand)s 업데이트 중", "Upgrade this room to version %(version)s": "이 방을 %(version)s 버전으로 업그레이드", @@ -1237,7 +1237,7 @@ "Private room (invite only)": "비공개 방 (초대 필요)", "Private (invite only)": "비공개 (초대 필요)", "Never send encrypted messages to unverified sessions in this room from this session": "이 채팅방의 현재 세션에서 확인되지 않은 세션으로 암호화된 메시지를 보내지 않음", - "Decide who can view and join %(spaceName)s.": "누가 %(spaceName)s를 보거나 참여할 수 있는지 결정합니다.", + "Decide who can view and join %(spaceName)s.": "누가 %(spaceName)s를 보거나 참여할 수 있는지 설정합니다.", "Accessibility": "접근성", "Access": "접근", "Recommended for public spaces.": "공개 스페이스에 권장 합니다.", @@ -1324,5 +1324,23 @@ "Direct Messages": "다이렉트 메세지", "Explore public rooms": "공개 방 목록 살펴보기", "Show %(count)s more|other": "%(count)s개 더 보기", - "People": "사람들" + "People": "사람들", + "If you can't see who you're looking for, send them your invite link below.": "찾으려는 사람이 보이지 않으면, 아래의 초대링크를 보내세요.", + "Some suggestions may be hidden for privacy.": "일부 추천 목록은 개인 정보 보호를 위해 보이지 않을 수 있습니다.", + "Start a conversation with someone using their name, email address or username (like ).": "이름, 이메일, 사용자명() 으로 대화를 시작하세요.", + "Search names and descriptions": "이름 및 설명 검색", + "Quick settings": "빠른 설정", + "@mentions & keywords": "@멘션(언급) & 키워드", + "Anyone in a space can find and join. You can select multiple spaces.": "스페이스의 누구나 찾고 참여할 수 있습니다. 여러 스페이스를 선택할 수 있습니다.", + "Anyone in a space can find and join. Edit which spaces can access here.": "누구나 스페이스를 찾고 참여할 수 있습니다. 이곳에서 접근 가능한 스페이스를 편집하세요.", + "Decide who can join %(roomName)s.": "%(roomName)s에 누가 참여할 수 있는지 설정합니다.", + "Add space": "스페이스 추가하기", + "Add existing rooms": "기존 방 추가", + "Add existing room": "기존 방 목록에서 추가하기", + "Invite to this space": "이 스페이스로 초대하기", + "Invite to space": "스페이스에 초대하기", + "Create a Group Chat": "그룹 대화 생성하기", + "Send a Direct Message": "다이렉트 메세지 보내기", + "Now, let's help you get started": "지금 시작할 수 있도록 도와드릴께요", + "Welcome %(name)s": "환영합니다 %(name)s님" } diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 875b855862d..d98c4fd8c89 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -588,7 +588,7 @@ "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.": "Viss du ikkje fjerna gjenopprettingsmetoden, kan ein angripar prøve å bryte seg inn på kontoen din. Endre kontopassordet ditt og sett ein opp ein ny gjenopprettingsmetode umidellbart under Innstillingar.", "Add Email Address": "Legg til e-postadresse", "Add Phone Number": "Legg til telefonnummer", - "Call failed due to misconfigured server": "Kallet gjekk gale fordi tenaren er oppsatt feil", + "Call failed due to misconfigured server": "Samtalen gjekk gale fordi tenaren er oppsatt feil", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Spør administratoren for din heimetenar%(homeserverDomain)s om å setje opp ein \"TURN-server\" slik at talesamtalar fungerer på rett måte.", "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativt, kan du prøva å nytta den offentlege tenaren på turn.matrix.org, men det kan vera mindre stabilt og IP-adressa di vil bli delt med den tenaren. Du kan og endra på det under Innstillingar.", "Try using turn.matrix.org": "Prøv med å nytta turn.matrix.org", @@ -1038,5 +1038,59 @@ "Review to ensure your account is safe": "Undersøk dette for å gjere kontoen trygg", "Review": "Undersøk", "Results are only revealed when you end the poll": "Resultatet blir synleg når du avsluttar røystinga", - "Results will be visible when the poll is ended": "Resultata vil bli synlege når røystinga er ferdig" + "Results will be visible when the poll is ended": "Resultata vil bli synlege når røystinga er ferdig", + "Start messages with /plain to send without markdown and /md to send with.": "Start meldingar med /plain for å senda utan markdown og /md for å senda med.", + "Enable Markdown": "Aktiver Markdown", + "Enable for this account": "Aktiver for denne kontoen", + "Hide sidebar": "Gøym sidestolpen", + "Show sidebar": "Vis sidestolpen", + "Close sidebar": "Lat att sidestolpen", + "Sidebar": "Sidestolpe", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Tillat å bruka Peer-to-Peer (P2P) for ein-til-ein samtalar (viss du aktiverer dette, kan det hende at motparten kan finne IP-adressa di)", + "Jump to the bottom of the timeline when you send a message": "Hopp til botn av tidslinja når du sender ei melding", + "Autoplay videos": "Spel av video automatisk", + "Autoplay GIFs": "Spel av GIF-ar automatisk", + "Expand map": "Utvid kart", + "Expand quotes": "Utvid sitat", + "Expand": "Utvid", + "Expand code blocks by default": "Utvid kodeblokker til vanleg", + "Enable": "Aktiver", + "All rooms you're in will appear in Home.": "Alle romma du er i vil vere synlege i Heim.", + "To view all keyboard shortcuts, click here.": "For å sjå alle tastatursnarvegane, klikk her.", + "Deactivate account": "Avliv brukarkontoen", + "Enter a new identity server": "Skriv inn ein ny identitetstenar", + "Rename": "Endra namn", + "Click the button below to confirm signing out these devices.|one": "Trykk på knappen under for å stadfesta utlogging frå denne eininga.", + "Confirm signing out these devices|one": "Stadfest utlogging frå denne eininga", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Stadfest utlogging av denne eininga ved å nytta Single-sign-on for å bevise identiteten din.", + "This device": "Denne eininga", + "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.": "Ved å endra passord på denne heimetenaren vil du bli logga ut frå alle andre einingar. Krypteringsnøklane som er lagra der vil bli sletta, og samtale-historikken din kan bli uleseleg.", + "Verify this device by confirming the following number appears on its screen.": "Verifiser denne eininga ved å stadfeste det følgjande talet når det kjem til syne på skjermen.", + "Quick settings": "Hurtigval", + "More options": "Fleire val", + "Pin to sidebar": "Fest til sidestolpen", + "Match system": "Følg systemet", + "You can change this at any time from room settings.": "Du kan endra dette kva tid som helst frå rominnstillingar.", + "Room settings": "Rominnstillingar", + "%(senderDisplayName)s changed who can join this room. View settings.": "%(senderDisplayName)s endra kven som kan bli med i rommet. Vis innstillingar.", + "Join the conference from the room information card on the right": "Bli med i konferanse frå rominfo-kortet til høgre", + "Room Info": "Rominfo", + "Final result based on %(count)s votes|one": "Endeleg resultat basert etter %(count)s stemme", + "Final result based on %(count)s votes|other": "Endeleg resultat basert etter %(count)s stemmer", + "That link is no longer supported": "Lenkja er ikkje lenger støtta", + "Failed to transfer call": "Overføring av samtalen feila", + "Transfer Failed": "Overføring feila", + "Unable to transfer call": "Fekk ikkje til å overføra samtalen", + "There was an error looking up the phone number": "Det skjedde ein feil under oppslag av telefonnummer", + "Unable to look up phone number": "Nummeroppslag gjekk gale", + "You've reached the maximum number of simultaneous calls.": "Du har nådd maksimalt tal samtidige samtalar.", + "You cannot place calls without a connection to the server.": "Du kan ikkje starta samtalar utan tilkopling til tenaren.", + "Connectivity to the server has been lost": "Tilkopling til tenaren vart tapt", + "You cannot place calls in this browser.": "Du kan ikkje samtala i nettlesaren.", + "Calls are unsupported": "Samtalar er ikkje støtta", + "Call failed because webcam or microphone could not be accessed. Check that:": "Samtalen gjekk gale fordi nettkamera eller mikrofon ikkje kunne aktiverast. Sjekk att:", + "No other application is using the webcam": "Ingen andre program brukar nettkameraet", + "Permission is granted to use the webcam": "Tilgang til nettkamera er aktivert", + "A microphone and webcam are plugged in and set up correctly": "Du har kopla til mikrofon og nettkamera, og at desse fungerer som dei skal", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Samtalen feila fordi mikrofonen ikkje kunne aktiverast. Sjekk att ein mikrofon er tilkopla og at den fungerer." } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 731c1ae1e78..8d462fa7c06 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -199,7 +199,7 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Serwer może być niedostępny, przeciążony, lub trafiłeś na błąd.", "Server unavailable, overloaded, or something else went wrong.": "Serwer może być niedostępny, przeciążony, lub coś innego poszło źle.", "Session ID": "Identyfikator sesji", - "Show timestamps in 12 hour format (e.g. 2:30pm)": "Pokaż czas w formacie 12-sto godzinnym (n.p. 2:30pm)", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Pokaż czas w formacie 12-sto godzinnym (np. 2:30pm)", "Signed Out": "Wylogowano", "Sign in": "Zaloguj", "Sign out": "Wyloguj", @@ -2108,5 +2108,7 @@ "In spaces %(space1Name)s and %(space2Name)s.": "W przestrzeniach %(space1Name)s i %(space2Name)s.", "Jump to the given date in the timeline": "Przeskocz do podanej daty w linii czasu", "Failed to invite users to %(roomName)s": "Nie udało się zaprosić użytkowników do %(roomName)s", - "That link is no longer supported": "Ten link nie jest już wspierany" + "That link is no longer supported": "Ten link nie jest już wspierany", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "Próbujesz dostać się do społeczności przez link (%(groupId)s).
    Społeczności nie są już wspierane i zostały zastąpione przez przestrzenie.Dowiedz się więcej o przestrzeniach tutaj.", + "Insert a trailing colon after user mentions at the start of a message": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 7c2d4ffb17b..00e339a8842 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -159,7 +159,7 @@ "Someone": "Кто-то", "Submit": "Отправить", "Success": "Успех", - "This email address is already in use": "Этот email уже используется", + "This email address is already in use": "Этот адрес электронной почты уже используется", "This email address was not found": "Этот адрес электронной почты не найден", "The email address linked to your account must be entered.": "Введите адрес электронной почты, связанный с вашей учётной записью.", "This room has no local addresses": "У этой комнаты нет адресов на вашем сервере", @@ -313,7 +313,7 @@ "Leave": "Покинуть", "Jump to read receipt": "Перейти к последнему прочтённому им сообщению", "Message Pinning": "Закреплённые сообщения", - "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) закреплённые в этой комнате сообщения.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) закреплённые сообщения в этой комнате.", "Unknown": "Неизвестно", "Loading...": "Загрузка...", "Unnamed room": "Комната без названия", @@ -480,7 +480,7 @@ "Event sent!": "Событие отправлено!", "Event Content": "Содержимое события", "Thank you!": "Спасибо!", - "Quote": "Цитата", + "Quote": "Цитировать", "Checking for an update...": "Проверка обновлений…", "Missing roomId.": "Отсутствует идентификатор комнаты.", "You don't currently have any stickerpacks enabled": "У вас ещё нет наклеек", @@ -584,7 +584,7 @@ "Encrypted messages in group chats": "Зашифрованные сообщения в групповых чатах", "The other party cancelled the verification.": "Другая сторона отменила проверку.", "Verified!": "Верифицировано!", - "You've successfully verified this user.": "Вы успешно верифицировали этого пользователя.", + "You've successfully verified this user.": "Вы успешно подтвердили этого пользователя.", "Got It": "Понятно", "Verify this user by confirming the following number appears on their screen.": "Подтвердите пользователя, убедившись, что на его экране отображается следующее число.", "Yes": "Да", @@ -726,9 +726,9 @@ "Anchor": "Якорь", "Headphones": "Наушники", "Folder": "Папка", - "Pin": "Кнопка", + "Pin": "Закрепить", "Your keys are being backed up (the first backup could take a few minutes).": "Выполняется резервная копия ключей (первый раз это может занять несколько минут).", - "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Размер файла '%(fileName)s' превышает ограничения сервера для загрузки", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Размер файла '%(fileName)s' превышает допустимый предел загрузки, установленный на этом сервере", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Добавляет смайл ¯\\_(ツ)_/¯ в начало сообщения", "Changes your display nickname in the current room only": "Изменяет ваш псевдоним только для текущей комнаты", "Gets or sets the room topic": "Читает или устанавливает тему комнаты", @@ -753,7 +753,7 @@ "Profile picture": "Аватар", "Accept all %(invitedRooms)s invites": "Принять все приглашения (%(invitedRooms)s)", "Missing media permissions, click the button below to request.": "Отсутствуют разрешения для доступа к камере/микрофону. Нажмите кнопку ниже, чтобы запросить их.", - "Request media permissions": "Запросить доступ к медиа носителю", + "Request media permissions": "Запросить доступ к медиа устройству", "Change room name": "Изменить название комнаты", "For help with using %(brand)s, click here.": "Для получения помощи по использованию %(brand)s, нажмите здесь.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Для получения помощи по использованию %(brand)s, нажмите здесь или начните чат с нашим ботом с помощью кнопки ниже.", @@ -786,12 +786,12 @@ "No homeserver URL provided": "URL-адрес домашнего сервера не указан", "Unexpected error resolving homeserver configuration": "Неожиданная ошибка в настройках домашнего сервера", "The user's homeserver does not support the version of the room.": "Домашний сервер пользователя не поддерживает версию комнаты.", - "Show read receipts sent by other users": "Показывать информацию о прочтении, посылаемую другими пользователями", + "Show read receipts sent by other users": "Показывать информацию о прочтении, посылаемые другими пользователями", "Show hidden events in timeline": "Показывать скрытые события в ленте сообщений", "When rooms are upgraded": "При обновлении комнат", "Upgrade to your own domain": "Обновление до собственного домена", "Credits": "Благодарности", - "Bulk options": "Групповые опции", + "Bulk options": "Основные опции", "Upgrade this room to the recommended room version": "Модернизируйте комнату до рекомендованной версии", "View older messages in %(roomName)s.": "Просмотр старых сообщений в %(roomName)s.", "Change history visibility": "Изменить видимость истории", @@ -800,7 +800,7 @@ "Invite users": "Пригласить пользователей", "Ban users": "Блокировка пользователей", "Send %(eventType)s events": "Отправить %(eventType)s события", - "Select the roles required to change various parts of the room": "Выберите роли, которые смогут менять различные параметры комнаты", + "Select the roles required to change various parts of the room": "Выберите роль, которая может изменять различные части комнаты", "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.": "После включения шифрования в комнате оно не может быть отключено. Сообщения, отправленные в шифрованной комнате, смогут прочитать только участники комнаты, но не сервер. Включенное шифрование может помешать корректной работе многим ботам и мостам. Подробнее о шифровании.", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Изменения в том, кто может читать историю, будут применяться только к будущим сообщениям в этой комнате. Существующие истории останутся без изменений.", "Once enabled, encryption cannot be disabled.": "После включения, шифрование не может быть отключено.", @@ -839,20 +839,20 @@ "Rotate Right": "Повернуть вправо", "Edit message": "Редактировать сообщение", "Power level": "Уровень прав", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не удалось найти профили для перечисленных ниже Matrix ID. Вы всё равно хотите их пригласить?", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не возможно найти профили для MatrixID, приведенных ниже - все равно желаете их пригласить?", "Invite anyway": "Всё равно пригласить", "GitHub issue": "GitHub вопрос", "Notes": "Заметка", "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.": "Если есть дополнительный контекст, который может помочь в анализе проблемы, такой как то, что вы делали в то время, ID комнат, ID пользователей и т. д., пожалуйста, включите эти данные.", - "Unable to load commit detail: %(msg)s": "Не удалось загрузить данные о коммите: %(msg)s", + "Unable to load commit detail: %(msg)s": "Не возможно загрузить детали подтверждения:: %(msg)s", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Чтобы не потерять историю чата, вы должны экспортировать ключи от комнаты перед выходом из системы. Для этого вам нужно будет вернуться к более новой версии %(brand)s", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Проверьте этого пользователя, чтобы отметить его как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Проверить этого пользователя, чтобы отметить его, как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.", "Waiting for partner to confirm...": "Ожидание подтверждения партнера...", "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Ранее вы использовали %(brand)s на %(host)s с отложенной загрузкой участников. В этой версии отложенная загрузка отключена. Поскольку локальный кеш не совместим между этими двумя настройками, %(brand)s необходимо повторно синхронизировать вашу учётную запись.", "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Если другая версия %(brand)s все еще открыта на другой вкладке, закройте ее, так как использование %(brand)s на том же хосте с включенной и отключенной ленивой загрузкой одновременно вызовет проблемы.", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s теперь использует в 3-5 раз меньше памяти, загружая информацию о других пользователях только когда это необходимо. Пожалуйста, подождите, пока мы ресинхронизируемся с сервером!", "I don't want my encrypted messages": "Мне не нужны мои зашифрованные сообщения", - "Manually export keys": "Экспортировать ключи вручную", + "Manually export keys": "Выгрузить ключи вручную", "Sign out and remove encryption keys?": "Выйти и удалить ключи шифрования?", "To help us prevent this in future, please send us logs.": "Чтобы помочь нам предотвратить это в будущем, пожалуйста, отправьте нам логи.", "Missing session data": "Отсутствуют данные сессии", @@ -1114,7 +1114,7 @@ "Objects": "Объекты", "Symbols": "Символы", "Flags": "Флаги", - "React": "Реакция", + "React": "Отреагировать", "Cancel search": "Отменить поиск", "Room %(name)s": "Комната %(name)s", "Jump to first unread room.": "Перейти в первую непрочитанную комнату.", @@ -1418,7 +1418,7 @@ "Looks good": "В порядке", "All rooms": "Все комнаты", "Your server": "Ваш сервер", - "Matrix": "Матрикс", + "Matrix": "Matrix", "Add a new server": "Добавить сервер", "Server name": "Имя сервера", "Destroy cross-signing keys?": "Уничтожить ключи кросс-подписи?", @@ -1584,8 +1584,8 @@ "Something went wrong trying to invite the users.": "Пытаясь пригласить пользователей, что-то пошло не так.", "We couldn't invite those users. Please check the users you want to invite and try again.": "Мы не могли пригласить этих пользователей. Пожалуйста, проверьте пользователей, которых вы хотите пригласить, и повторите попытку.", "Failed to find the following users": "Не удалось найти этих пользователей", - "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Следующие пользователи могут не существовать или быть недействительными и не могут быть приглашены: %(csvNames)s", - "Recently Direct Messaged": "Недавно отправленные личные сообщения", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Следующие пользователи могут не существовать, или быть недействительными и не могут быть приглашены: %(csvNames)s", + "Recently Direct Messaged": "Последние прямые сообщения", "Go": "Вперёд", "a new master key signature": "новая подпись мастер-ключа", "a new cross-signing key signature": "новый ключ подписи для кросс-подписи", @@ -1720,7 +1720,7 @@ "%(count)s results|one": "%(count)s результат", "Room Info": "Информация о комнате", "Not encrypted": "Не зашифровано", - "About": "О приложение", + "About": "О комнате", "Room settings": "Настройки комнаты", "Take a picture": "Сделать снимок", "Unpin": "Открепить", @@ -1782,7 +1782,7 @@ "Send stickers into this room": "Отправить стикеры в эту комнату", "Use Ctrl + Enter to send a message": "Используйте Ctrl + Enter, чтобы отправить сообщение", "Use Command + Enter to send a message": "Используйте Command + Enter, чтобы отправить сообщение", - "Go to Home View": "Домой", + "Go to Home View": "Перейти на Главную", "Start a new chat": "Начать новый чат", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Сообщения в этой комнате полностью зашифрованы. Когда люди присоединяются, вы можете проверить их в их профиле, просто нажмите на их аватар.", "This is the start of .": "Это начало .", @@ -1792,7 +1792,7 @@ "Add a topic to help people know what it is about.": "Добавьте тему, чтобы люди знали, о чём комната.", "Topic: %(topic)s ": "Тема: %(topic)s ", "Topic: %(topic)s (edit)": "Тема: %(topic)s (изменить)", - "This is the beginning of your direct message history with .": "Это начало вашей переписки с .", + "This is the beginning of your direct message history with .": "Это начало вашей беседы с .", "Only the two of you are in this conversation, unless either of you invites anyone to join.": "В этом разговоре только вы двое, если только кто-нибудь из вас не пригласит кого-нибудь присоединиться.", "Takes the call in the current room off hold": "Прекратить удержание вызова в текущей комнате", "Places the call in the current room on hold": "Перевести вызов в текущей комнате на удержание", @@ -2281,7 +2281,7 @@ "Failed to save space settings.": "Не удалось сохранить настройки пространства.", "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.": "Обычно это только влияет на то, как комната обрабатывается на сервере. Если у вас проблемы с вашим %(brand)s, сообщите об ошибке.", "Invite someone using their name, email address, username (like ) or share this space.": "Пригласите кого-нибудь, используя их имя, адрес электронной почты, имя пользователя (например, ) или поделитесь этим пространством.", - "Invite someone using their name, username (like ) or share this space.": "Пригласите кого-нибудь, используя их имя, имя пользователя (как ) или поделитесь этим пространством.", + "Invite someone using their name, username (like ) or share this space.": "Пригласите кого-нибудь, используя их имя, учётную запись (как ) или поделитесь этим пространством.", "Invite to %(roomName)s": "Пригласить в %(roomName)s", "Unnamed Space": "Безымянное пространство", "Invite to %(spaceName)s": "Пригласить в %(spaceName)s", @@ -2322,16 +2322,16 @@ "Delete": "Удалить", "Jump to the bottom of the timeline when you send a message": "Перейти к нижней части временной шкалы, когда вы отправляете сообщение", "Check your devices": "Проверьте ваши устройства", - "You have unverified logins": "У вас есть непроверенные входы в систему", + "You have unverified logins": "У вас есть не проверенные входы в систему", "This homeserver has been blocked by its administrator.": "Доступ к этому домашнему серверу заблокирован вашим администратором.", "You're already in a call with this person.": "Вы уже разговариваете с этим человеком.", "Already in call": "Уже в вызове", "Original event source": "Оригинальный исходный код", "Decrypted event source": "Расшифрованный исходный код", - "Values at explicit levels in this room:": "Значения уровня чувствительности в этой комнате:", - "Values at explicit levels:": "Значения уровня чувствительности:", - "Values at explicit levels in this room": "Значения уровня чувствительности в этой комнате", - "Values at explicit levels": "Значения уровня чувствительности", + "Values at explicit levels in this room:": "Значения на явных уровнях:", + "Values at explicit levels:": "Значения на явных уровнях:", + "Values at explicit levels in this room": "Значения на явных уровнях в этой комнате", + "Values at explicit levels": "Значения на явных уровнях", "Invite by username": "Пригласить по имени пользователя", "Make sure the right people have access. You can invite more later.": "Убедитесь, что правильные люди имеют доступ. Вы можете пригласить больше людей позже.", "Invite your teammates": "Пригласите своих товарищей по команде", @@ -2374,22 +2374,22 @@ "Integration manager": "Менеджер интеграции", "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не позволяет вам использовать для этого Менеджер Интеграции. Пожалуйста, свяжитесь с администратором.", "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Используя этот виджет, вы можете делиться данными с %(widgetDomain)s и вашим Менеджером Интеграции.", - "Identity server is": "Сервер идентификации", - "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджеры интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.", + "Identity server is": "Идентификационный сервер", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджеры по интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.", "Use an integration manager to manage bots, widgets, and sticker packs.": "Используйте менеджер интеграций для управления ботами, виджетами и наклейками.", "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и наклейками.", - "Identity server": "Сервер идентификаций", - "Identity server (%(server)s)": "Сервер идентификации (%(server)s)", + "Identity server": "Идентификационный сервер", + "Identity server (%(server)s)": "Идентификационный сервер (%(server)s)", "Could not connect to identity server": "Не удалось подключиться к серверу идентификации", - "Not a valid identity server (status code %(code)s)": "Неправильный Сервер идентификации (код статуса %(code)s)", - "Identity server URL must be HTTPS": "URL-адрес сервера идентификации должен быть HTTPS", + "Not a valid identity server (status code %(code)s)": "Недействительный идентификационный сервер (код состояния %(code)s)", + "Identity server URL must be HTTPS": "URL-адрес идентификационного сервер должен начинаться с HTTPS", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Общение с %(transferTarget)s. Перевод на %(transferee)s", - "[number]": "[цифра]", + "[number]": "[количество]", "Enter your Security Phrase a second time to confirm it.": "Введите секретную фразу второй раз, чтобы подтвердить ее.", "Space Autocomplete": "Автозаполнение пространства", "Verify your identity to access encrypted messages and prove your identity to others.": "Проверьте свою личность, чтобы получить доступ к зашифрованным сообщениям и доказать свою личность другим.", - "Currently joining %(count)s rooms|one": "Сейчас вы присоединяетесь к %(count)s комнате", - "Currently joining %(count)s rooms|other": "Сейчас вы присоединяетесь к %(count)s комнатам", + "Currently joining %(count)s rooms|one": "Сейчас вы состоите в %(count)s комнате", + "Currently joining %(count)s rooms|other": "Сейчас вы состоите в %(count)s комнатах", "You can add more later too, including already existing ones.": "Позже можно добавить и другие, в том числе уже существующие.", "Let's create a room for each of them.": "Давайте создадим для каждого из них отдельную комнату.", "What are some things you want to discuss in %(spaceName)s?": "Какие вещи вы хотите обсуждать в %(spaceName)s?", @@ -2406,7 +2406,7 @@ "Some of your messages have not been sent": "Некоторые из ваших сообщений не были отправлены", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Попробуйте использовать другие слова или проверьте опечатки. Некоторые результаты могут быть не видны, так как они приватные и для участия в них необходимо приглашение.", "No results for \"%(query)s\"": "Нет результатов для \"%(query)s\"", - "Verification requested": "Запрос на проверку отправлен", + "Verification requested": "Запрошено подтверждение", "Unable to copy a link to the room to the clipboard.": "Не удалось скопировать ссылку на комнату в буфер обмена.", "Unable to copy room link": "Не удалось скопировать ссылку на комнату", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас.", @@ -2438,7 +2438,7 @@ "Spam or propaganda": "Спам или пропаганда", "Illegal Content": "Незаконный контент", "Toxic Behaviour": "Токсичное поведение", - "Disagree": "Я не согласен с содержанием", + "Disagree": "Не согласен", "Please pick a nature and describe what makes this message abusive.": "Пожалуйста, выберите характер и опишите, что делает это сообщение оскорбительным.", "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Любая другая причина. Пожалуйста, опишите проблему.\nОб этом будет сообщено модераторам комнаты.", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Эта комната посвящена незаконному или токсичному контенту, или модераторы не справляются с модерацией незаконного или токсичного контента.\n Об этом будет сообщено администраторам %(homeserver)s.", @@ -2452,7 +2452,7 @@ "Spaces you know that contain this room": "Пространства, которые вы знаете, уже содержат эту комнату", "Search spaces": "Поиск пространств", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Определите, какие пространства могут получить доступ к этой комнате. Если пространство выбрано, его члены могут найти и присоединиться к .", - "Select spaces": "Выберите пространства", + "Select spaces": "Выберите места", "You're removing all spaces. Access will default to invite only": "Вы удаляете все пространства. Доступ будет по умолчанию только по приглашениям", "Leave %(spaceName)s": "Покинуть %(spaceName)s", "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "Вы являетесь единственным администратором некоторых комнат или пространств, которые вы хотите покинуть. Покинув их, вы оставите их без администраторов.", @@ -2460,7 +2460,7 @@ "You won't be able to rejoin unless you are re-invited.": "Вы сможете присоединиться только после повторного приглашения.", "Search %(spaceName)s": "Поиск %(spaceName)s", "User Directory": "Каталог пользователей", - "Consult first": "Сначала спросить", + "Consult first": "Сначала проконсультируйтесь", "Invited people will be able to read old messages.": "Приглашенные люди смогут читать старые сообщения.", "Or send invite link": "Или отправьте ссылку на приглашение", "Some suggestions may be hidden for privacy.": "Некоторые предложения могут быть скрыты в целях конфиденциальности.", @@ -2533,13 +2533,13 @@ "An unknown error occurred": "Произошла неизвестная ошибка", "Their device couldn't start the camera or microphone": "Их устройство не может запустить камеру или микрофон", "Connection failed": "Ошибка соединения", - "Could not connect media": "Сбой подключения", + "Could not connect media": "Не удалось подключиться к носителю", "Missed call": "Пропущенный вызов", "Call back": "Перезвонить", "Call declined": "Вызов отклонён", - "Pinned messages": "Прикреплённые сообщения", - "If you have permissions, open the menu on any message and select Pin to stick them here.": "Если у вас есть разрешения, откройте меню на любом сообщении и выберите Прикрепить, чтобы поместить их сюда.", - "Nothing pinned, yet": "Пока ничего не прикреплено", + "Pinned messages": "Закреплённые сообщения", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Если у вас есть разрешения, откройте меню на любом сообщении и выберите Закрепить, чтобы поместить их сюда.", + "Nothing pinned, yet": "Пока ничего не закреплено", "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Установите адреса для этого пространства, чтобы пользователи могли найти это пространство через ваш домашний сервер (%(localDomain)s)", "To publish an address, it needs to be set as a local address first.": "Чтобы опубликовать адрес, его сначала нужно установить как локальный адрес.", "Published addresses can be used by anyone on any server to join your room.": "Опубликованные адреса могут быть использованы любым человеком на любом сервере, чтобы присоединиться к вашей комнате.", @@ -2618,7 +2618,7 @@ "Your camera is still enabled": "Ваша камера всё ещё включена", "Your camera is turned off": "Ваша камера выключена", "%(sharerName)s is presenting": "%(sharerName)s показывает", - "You are presenting": "Вы показываете", + "You are presenting": "Вы представляете", "unknown person": "Неизвестное лицо", "Mute the microphone": "Заглушить микрофон", "Unmute the microphone": "Включить звук микрофона", @@ -2631,7 +2631,7 @@ "Stop the camera": "Остановить камеру", "Start the camera": "Запуск камеры", "sends space invaders": "отправляет космических захватчиков", - "Sends the given message with a space themed effect": "Отправляет заданное сообщение с космическим тематическим эффектом", + "Sends the given message with a space themed effect": "Отправить данное сообщение с эффектом космоса", "All rooms you're in will appear in Home.": "Все комнаты, в которых вы находитесь, будут отображаться в Начале.", "Show all rooms in Home": "Показывать все комнаты в Начале", "Surround selected text when typing special characters": "Обводить выделенный текст при вводе специальных символов", @@ -2643,7 +2643,7 @@ "Review to ensure your account is safe": "Проверьте, чтобы убедиться, что ваша учетная запись в безопасности", "See when people join, leave, or are invited to your active room": "Просмотрите, когда люди присоединяются, уходят или приглашают в вашу активную комнату", "See when people join, leave, or are invited to this room": "Посмотрите, когда люди присоединяются, покидают или приглашают в эту комнату", - "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) прикреплённые сообщения для комнаты.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) закреплённые сообщения в этой комнате.", "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s отозвал(а) приглашение %(targetName)s", "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s отозвал(а) приглашение %(targetName)s: %(reason)s", "%(senderName)s unbanned %(targetName)s": "%(senderName)s разблокировал(а) %(targetName)s", @@ -2700,10 +2700,10 @@ "Autoplay GIFs": "Автовоспроизведение GIF", "The above, but in as well": "Вышеописанное, но также в ", "The above, but in any room you are joined or invited to as well": "Вышеперечисленное, но также в любой комнате, в которую вы вошли или приглашены", - "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s открепляет сообщение из этой комнаты. Просмотрите все прикрепленые сообщения.", - "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s открепляет сообщение из этой комнаты. Просмотрите все прикрепленые сообщения.", - "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s прикрепляет сообщение в этой комнате. Просмотрите все прикрепленные сообщения.", - "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s прикрепляет сообщение в этой комнате. Просмотрите все прикрепленые сообщения.", + "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s открепил(а) сообщение в этой комнате. Посмотрите все закреплённые сообщения.", + "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s открепил(а) сообщение в этой комнате. Посмотрите все закреплённые сообщения.", + "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s закрепил(а) сообщение в этой комнате. Посмотрите все закреплённые сообщения.", + "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s закрепил(а) сообщение в этой комнате. Посмотрите все закреплённые сообщения.", "Leave some rooms": "Покинуть несколько комнат", "Leave all rooms": "Покинуть все комнаты", "Don't leave any rooms": "Не покидать ни одну комнату", @@ -2756,8 +2756,8 @@ "Verify with Security Key": "Проверить с помощью ключа безопасности", "Verify with Security Key or Phrase": "Проверка с помощью ключа безопасности или фразы", "Proceed with reset": "Выполнить сброс", - "It looks like you don't have a Security 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.": "Похоже, что у вас нет Ключа безопасности или других устройств, которые можно проверить. Это устройство не сможет получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность на этом устройстве, вам необходимо сбросить ключи проверки.", - "Really reset verification keys?": "Действительно сбросить ключи проверки?", + "It looks like you don't have a Security 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.": "Похоже, у вас нет ключа шифрования, или каких-либо других устройств, которые вы можете проверить. Это устройство не сможет получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность на этом устройстве, вам потребуется сбросить ключи подтверждения.", + "Really reset verification keys?": "Действительно сбросить ключи подтверждения?", "Create poll": "Создать опрос", "Thread": "Обсуждение", "Reply to thread…": "Ответить на обсуждение…", @@ -2847,7 +2847,7 @@ "Someone already has that username. Try another or if it is you, sign in below.": "У кого-то уже есть такое имя пользователя. Попробуйте другое или, если это вы, войдите ниже.", "Unable to check if username has been taken. Try again later.": "Не удалось проверить, занято ли имя пользователя. Повторите попытку позже.", "Thread options": "Параметры обсуждения", - "Space home": "Дом пространства", + "Space home": "Место дома", "See room timeline (devtools)": "Просмотреть шкалу времени комнаты (инструменты разработчика)", "Mentions only": "Только упоминания", "Forget": "Забыть", @@ -2903,7 +2903,7 @@ "were removed %(count)s times|one": "было удалено", "were removed %(count)s times|other": "удалены %(count)s раз(а)", "Including you, %(commaSeparatedMembers)s": "Включая вас, %(commaSeparatedMembers)s", - "Backspace": "Backspace", + "Backspace": "Очистить", "Unknown error fetching location. Please try again later.": "Неизвестная ошибка при получении местоположения. Пожалуйста, повторите попытку позже.", "Timed out trying to fetch your location. Please try again later.": "Попытка определить ваше местоположение завершилась. Пожалуйста, повторите попытку позже.", "Failed to fetch your location. Please try again later.": "Не удалось определить ваше местоположение. Пожалуйста, повторите попытку позже.", @@ -2952,7 +2952,7 @@ "To proceed, please accept the verification request on your other device.": "Чтобы продолжить, пожалуйста, примите запрос на проверку на другом устройстве.", "Copy room link": "Скопировать ссылку на комнату", "You were removed from %(roomName)s by %(memberName)s": "%(memberName)s удалил(а) вас из %(roomName)s", - "Home options": "Параметры дома", + "Home options": "Параметры Главной", "%(spaceName)s menu": "Меню %(spaceName)s", "Join public room": "Присоединиться к публичной комнате", "Can't see what you're looking for?": "Не нашли ничего?", @@ -2980,14 +2980,14 @@ "@mentions & keywords": "@упоминания и ключевые слова", "Get notified for every message": "Получать уведомление о каждом сообщении", "Get notifications as set up in your settings": "Получать уведомления в соответствии с настройками", - "This room isn't bridging messages to any platforms. Learn more.": "Эта комната не передаёт сообщения каким-либо платформам. Подробнее.", + "This room isn't bridging messages to any platforms. Learn more.": "Эта комната не передаёт сообщения на какие-либо платформы Узнать больше.", "Internal room ID": "Внутренний ID комнаты", "Group all your rooms that aren't part of a space in one place.": "Сгруппируйте все комнаты, которые не являются частью пространства, в одном месте.", "Rooms outside of a space": "Комнаты без пространства", "Group all your people in one place.": "Сгруппируйте всех своих людей в одном месте.", "Group all your favourite rooms and people in one place.": "Сгруппируйте все свои любимые комнаты и людей в одном месте.", - "Show all your rooms in Home, even if they're in a space.": "Показать все комнаты в доме, даже если они находятся в одном пространстве.", - "Home is useful for getting an overview of everything.": "Дом полезен для получения общего представления обо всем.", + "Show all your rooms in Home, even if they're in a space.": "Покажите все свои комнаты в главной, даже если они находятся в пространстве.", + "Home is useful for getting an overview of everything.": "Главная полезна для получения общего представления обо всем.", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Пространства - это способ группировки комнат и людей. Наряду с пространствами, в которых вы находитесь, вы также можете использовать некоторые предварительно созданные пространства.", "Spaces to show": "Пространства для показа", "Sidebar": "Боковая панель", @@ -3065,7 +3065,7 @@ "Creating HTML...": "Создание HTML…", "Fetched %(count)s events in %(seconds)ss|one": "Получено %(count)s событие за %(seconds)sс", "Fetched %(count)s events so far|one": "Получено %(count)s событие", - "Fetched %(count)s events out of %(total)s|one": "Получено %(count)s из %(total)s события", + "Fetched %(count)s events out of %(total)s|one": "Извлечено %(count)s из %(total)s события", "Fetched %(count)s events in %(seconds)ss|other": "Получено %(count)s событий за %(seconds)sс", "Starting export...": "Начало экспорта…", "Processing event %(number)s out of %(total)s": "Обработано %(number)s из %(total)s событий", @@ -3140,8 +3140,8 @@ "We couldn't send your location": "Мы не смогли отправить ваше местоположение", "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "Этот домашний сервер неправильно настроен для отображения карт, или настроенный сервер карт может быть недоступен.", "This homeserver is not configured to display maps.": "Этот домашний сервер не настроен на отображение карт.", - "Click to drop a pin": "Нажмите, чтобы создать маркер", - "Click to move the pin": "Нажмите, чтобы сдвинуть маркер", + "Click to drop a pin": "Нажмите, чтобы закрепить маркер", + "Click to move the pin": "Нажмите, чтобы переместить маркер", "Results will be visible when the poll is ended": "Результаты будут видны после завершения опроса", "Sorry, you can't edit a poll after votes have been cast.": "Вы не можете редактировать опрос после завершения голосования.", "Can't edit poll": "Невозможно редактировать опрос", @@ -3157,7 +3157,7 @@ "Open thread": "Открыть ветку", "You do not have permissions to add spaces to this space": "У вас нет разрешения добавлять пространства в это пространство", "Remove messages sent by me": "Удалить отправленные мной сообщения", - "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "Пространства – это новый способ групповых комнат и людей. Какой вид пространства вы хотите создать? Вы можете изменить это позже.", + "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "Пространства — это новый способ организации комнат и людей. Какой вид пространства вы хотите создать? Вы можете изменить это позже.", "Match system": "Как в системе", "Automatically send debug logs when key backup is not functioning": "Автоматически отправлять журналы отладки, когда резервное копирование ключей не работает", "Insert a trailing colon after user mentions at the start of a message": "Вставлять двоеточие после упоминания пользователя в начале сообщения", @@ -3296,7 +3296,7 @@ "Observe only": "Только наблюдать", "Requester": "Адресат", "Methods": "Методы", - "Timeout": "Ограничение времени", + "Timeout": "Тайм-аут", "Phase": "Фаза", "Transaction": "Транзакция", "Cancelled": "Отменено", @@ -3442,5 +3442,82 @@ "Exit fullscreen": "Выйти из полноэкранного режима", "In %(spaceName)s and %(count)s other spaces.|one": "В %(spaceName)s и %(count)s другом пространстве.", "In %(spaceName)s and %(count)s other spaces.|other": "В %(spaceName)s и %(count)s других пространствах.", - "In spaces %(space1Name)s and %(space2Name)s.": "В пространствах %(space1Name)s и %(space2Name)s." + "In spaces %(space1Name)s and %(space2Name)s.": "В пространствах %(space1Name)s и %(space2Name)s.", + "Use new session manager (under active development)": "Использовать новый менеджер сессий (в активной разработке)", + "Unverified": "Не подтверждено", + "Verified": "Подтверждено", + "IP address": "IP-адрес", + "Device": "Устройство", + "Last activity": "Последняя активность", + "Other sessions": "Другие сессии", + "Current session": "Текущая сессия", + "Sessions": "Сессии", + "Unverified session": "Неподтверждённая сессия", + "Verified session": "Подтверждённая сессия", + "Android": "Android", + "iOS": "iOS", + "We'll help you get connected.": "Мы поможем вам подключиться.", + "Join the room to participate": "Присоединяйтесь к комнате для участия", + "This session is ready for secure messaging.": "Эта сессия готова к безопасному обмену сообщениями.", + "Start your first chat": "Начните свою первую беседу", + "We're creating a room with %(names)s": "Мы создаем комнату с %(names)s", + "You can't disable this later. The room will be encrypted but the embedded call will not.": "Вы не сможете отключить это позже. Комната будет зашифрована, а встроенный вызов — нет.", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play и Логотип Google Play являются торговыми знаками Google LLC.", + "App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® и Логотип Apple® являются товарными знаками Apple Inc.", + "Get it on Google Play": "Скачать в Google Play", + "Get it on F-Droid": "Скачать на F-Droid", + "Download %(brand)s": "Скачать %(brand)s", + "Download %(brand)s Desktop": "Скачать %(brand)s Desktop", + "Download on the App Store": "Скачать в App Store", + "Online community members": "Участники сообщества в сети", + "You're in": "Вы в", + "Choose a locale": "Выберите регион", + "Help": "Помощь", + "You need to have the right permissions in order to share locations in this room.": "У вас должны быть определённые разрешения, чтобы делиться местоположениями в этой комнате.", + "You don't have permission to share locations": "У вас недостаточно прав для публикации местоположений", + "Send your first message to invite to chat": "Отправьте свое первое сообщение, чтобы пригласить в чат", + "Inactive for %(inactiveAgeDays)s+ days": "Неактивен в течение %(inactiveAgeDays)s+ дней", + "Session details": "Сведения о сеансе", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "В целях безопасности подтвердите свои сеансы и выйдите из любого сеанса, который вы больше не знаете или не используете.", + "Verify or sign out from this session for best security and reliability.": "Подтвердите или выйдите из этого сеанса для обеспечения максимальной безопасности и надежности.", + "Your server doesn't support disabling sending read receipts.": "Ваш сервер не поддерживает отключение отправки уведомлений о прочтении.", + "Share your activity and status with others.": "Поделитесь своей активностью и статусом с другими.", + "Presence": "Присутствие", + "Complete these to get the most out of %(brand)s": "Выполните их, чтобы получить максимальную отдачу от %(brand)s", + "You did it!": "Вы сделали это!", + "Only %(count)s steps to go|one": "Осталось всего %(count)s шагов до конца", + "Only %(count)s steps to go|other": "Осталось всего %(count)s шагов", + "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Сохраняйте право над владением и контроль над обсуждением в сообществе.\nМасштабируйте, чтобы поддерживать миллионы, с мощной модерацией и функциональной совместимостью.", + "Community ownership": "Владение сообществом", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "Благодаря бесплатному сквозному шифрованному обмену сообщениями и неограниченным голосовым и видеозвонкам, %(brand)s это отличный способ оставаться на связи.", + "Secure messaging for friends and family": "Безопасный обмен сообщениями для друзей и семьи", + "We’d appreciate any feedback on how you’re finding Element.": "Мы будем признательны за любые отзывы о том, как вы находите Element.", + "How are you finding Element so far?": "Как вы находите Element до сих пор?", + "Don’t miss a reply or important message": "Не пропустите ответ или важное сообщение", + "Turn on notifications": "Включить уведомления", + "Make sure people know it’s really you": "Убедитесь, что люди знают, что это действительно вы", + "Download apps": "Скачать приложения", + "Don’t miss a thing by taking Element with you": "Не пропустите ничего, взяв с собой Element", + "Download Element": "Скачать элемент", + "Find and invite your community members": "Найдите и пригласите участников сообщества", + "Find people": "Найти людей", + "Get stuff done by finding your teammates": "Добейтесь успеха, найдя своих товарищей по команде", + "Find and invite your co-workers": "Найдите и пригласите своих коллег", + "Find friends": "Найти друзей", + "It’s what you’re here for, so lets get to it": "Это то, для чего вы здесь, так что давайте приступим к делу", + "Find and invite your friends": "Найдите и пригласите своих друзей", + "You made it!": "Вы сделали это!", + "Show shortcut to welcome checklist above the room list": "Показывать ярлык приветственного проверенного списка над списком комнат", + "Send read receipts": "Отправлять уведомления о прочтении", + "Reset bearing to north": "Сбросить пеленг на север", + "Toggle attribution": "Переключить атрибуцию", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "Команда разработчика: Отменить текущий сеанс исходящей группы и настроить новые сеансы Olm", + "Set up your profile": "Настройте свой профиль", + "Welcome": "Добро пожаловать", + "Improve your account security by following these recommendations": "Повысьте безопасность учётной записи, следуя этим рекомендациям", + "Security recommendations": "Рекомендации по безопасности", + "Inactive sessions": "Неактивные сессии", + "Unverified sessions": "Неподтверждённые сессии", + "All": "Все", + "Verified sessions": "Подтверждённые сессии" } diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 05285a15234..f4a16e2a819 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -3150,7 +3150,7 @@ "Match system": "Zhoda so systémom", "The %(capability)s capability": "Schopnosť %(capability)s", "Host account on": "Hostiteľský účet na", - "Joined": "Sa pripojil/a", + "Joined": "Ste pripojený", "Resend %(unsentCount)s reaction(s)": "Opätovné odoslanie %(unsentCount)s reakcií", "Consult first": "Najprv konzultovať", "Values at explicit levels in this room:": "Hodnoty na explicitných úrovniach v tejto miestnosti:", @@ -3497,5 +3497,45 @@ "Send read receipts": "Odosielať potvrdenia o prečítaní", "Last activity": "Posledná aktivita", "Sessions": "Relácie", - "Use new session manager (under active development)": "Použiť nového správcu relácií (v štádiu aktívneho vývoja)" + "Use new session manager (under active development)": "Použiť nového správcu relácií (v štádiu aktívneho vývoja)", + "Current session": "Aktuálna relácia", + "Unverified": "Neoverené", + "Verified": "Overený", + "Session details": "Podrobnosti o relácii", + "IP address": "IP adresa", + "Device": "Zariadenie", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.", + "Other sessions": "Iné relácie", + "Verify or sign out from this session for best security and reliability.": "V záujme čo najvyššej bezpečnosti a spoľahlivosti túto reláciu overte alebo sa z nej odhláste.", + "Unverified session": "Neoverená relácia", + "This session is ready for secure messaging.": "Táto relácia je pripravená na bezpečné zasielanie správ.", + "Verified session": "Overená relácia", + "Inactive for %(inactiveAgeDays)s+ days": "Neaktívny počas %(inactiveAgeDays)s+ dní", + "Welcome": "Vitajte", + "Show shortcut to welcome checklist above the room list": "Zobraziť skratku na uvítací kontrolný zoznam nad zoznamom miestností", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Zvážte odhlásenie zo starých relácií (%(inactiveAgeDays)s dní alebo starších), ktoré už nepoužívate", + "Inactive sessions": "Neaktívne relácie", + "View all": "Zobraziť všetky", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Overte si relácie pre vylepšené bezpečné zasielanie správ alebo sa odhláste z tých, ktoré už nepoznáte alebo nepoužívate.", + "Unverified sessions": "Neoverené relácie", + "Improve your account security by following these recommendations": "Zlepšite zabezpečenie svojho účtu dodržiavaním týchto odporúčaní", + "Security recommendations": "Bezpečnostné odporúčania", + "Interactively verify by emoji": "Interaktívne overte pomocou emotikonov", + "Manually verify by text": "Manuálne overte pomocou textu", + "Filter devices": "Filtrovať zariadenia", + "Inactive for %(inactiveAgeDays)s days or longer": "Neaktívny %(inactiveAgeDays)s dní alebo dlhšie", + "Inactive": "Neaktívne", + "Not ready for secure messaging": "Nie je pripravené na bezpečné zasielanie správ", + "Ready for secure messaging": "Pripravené na bezpečné zasielanie správ", + "All": "Všetky", + "No sessions found.": "Nenašli sa žiadne relácie.", + "No inactive sessions found.": "Nenašli sa žiadne neaktívne relácie.", + "No unverified sessions found.": "Nenašli sa žiadne neoverené relácie.", + "No verified sessions found.": "Nenašli sa žiadne overené relácie.", + "For best security, sign out from any session that you don't recognize or use anymore.": "V záujme čo najlepšieho zabezpečenia sa odhláste z každej relácie, ktorú už nepoznáte alebo nepoužívate.", + "Verified sessions": "Overené relácie", + "Toggle device details": "Prepínanie údajov o zariadení", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "Budeme vďační za akúkoľvek spätnú väzbu o tom, ako sa vám %(brand)s osvedčil.", + "How are you finding %(brand)s so far?": "Ako sa vám zatiaľ páči %(brand)s?", + "Don’t miss a thing by taking %(brand)s with you": "Nezmeškáte nič, ak so sebou vezmete %(brand)s" } diff --git a/src/i18n/strings/te.json b/src/i18n/strings/te.json index 6617b3c0e85..16f661deeaa 100644 --- a/src/i18n/strings/te.json +++ b/src/i18n/strings/te.json @@ -3,7 +3,7 @@ "Account": "ఖాతా", "Add": "చేర్చు", "Admin": "అడ్మిన్", - "Admin Tools": "నిర్వాహక ఉపకరణాలు", + "Admin Tools": "నిర్వాహకుని ఉపకరణాలు", "No Microphones detected": "మైక్రోఫోన్లు కనుగొనబడలేదు", "No Webcams detected": "వెబ్కామ్లు కనుగొనబడలేదు", "No media permissions": "మీడియా అనుమతులు లేవు", @@ -104,7 +104,7 @@ "remove %(name)s from the directory.": "వివరము నుండి %(name)s ను తొలిగించు.", "Failed to add tag %(tagName)s to room": "%(tagName)s ను బొందు జోడించడంలో విఫలమైంది", "No update available.": "ఏ నవీకరణ అందుబాటులో లేదు.", - "Resend": "మళ్ళి పంపుము", + "Resend": "మళ్ళి పంపుము", "Collecting app version information": "అనువర్తన సంస్కరణ సమాచారాన్ని సేకరించడం", "Tuesday": "మంగళవారం", "Remove %(name)s from the directory?": "వివరము నుండి %(name)s తొలిగించు?", @@ -134,5 +134,7 @@ "This email address is already in use": "ఈ ఇమెయిల్ అడ్రస్ ఇప్పటికే వాడుకం లో ఉంది", "This phone number is already in use": "ఈ ఫోన్ నంబర్ ఇప్పటికే వాడుకం లో ఉంది", "Failed to verify email address: make sure you clicked the link in the email": "ఇమెయిల్ అడ్రస్ ని నిరూపించలేక పోయాము. ఈమెయిల్ లో వచ్చిన లింక్ ని నొక్కారా", - "Call Failed": "కాల్ విఫలమయింది" + "Call Failed": "కాల్ విఫలమయింది", + "Confirm adding email": "ఈమెయిల్ చేర్చుటకు ధ్రువీకరించు", + "Single Sign On": "సింగిల్ సైన్ ఆన్" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index d86168051bc..db3272e28ac 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -26,7 +26,7 @@ "No Microphones detected": "Мікрофон не виявлено", "No Webcams detected": "Веб-камеру не виявлено", "Favourites": "Вибрані", - "No media permissions": "Нема дозволів на відео/аудіо", + "No media permissions": "Немає медіадозволів", "You may need to manually permit %(brand)s to access your microphone/webcam": "Можливо, вам треба дозволити %(brand)s використання мікрофону/камери вручну", "Default Device": "Уставний пристрій", "Microphone": "Мікрофон", @@ -78,7 +78,7 @@ "This Room": "Ця кімната", "Noisy": "Шумно", "Messages containing my display name": "Повідомлення, що містять моє видиме ім'я", - "Unavailable": "Нема в наявності", + "Unavailable": "Недоступний", "remove %(name)s from the directory.": "прибрати %(name)s з каталогу.", "Source URL": "Початкова URL-адреса", "Messages sent by bot": "Повідомлення, надіслані ботом", @@ -315,7 +315,7 @@ "Send an encrypted message…": "Надіслати зашифроване повідомлення…", "The conversation continues here.": "Розмова триває тут.", "This room has been replaced and is no longer active.": "Ця кімната була замінена і не є активною.", - "You do not have permission to post to this room": "У вас нема дозволу дописувати у цю кімнату", + "You do not have permission to post to this room": "У вас немає дозволу писати в цій кімнаті", "Sign out": "Вийти", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Щоб уникнути втрати історії ваших листувань, ви маєте експортувати ключі кімнати перед виходом. Вам треба буде повернутися до новішої версії %(brand)s аби зробити це", "Incompatible Database": "Несумісна база даних", @@ -2086,7 +2086,7 @@ "Sign Out": "Вийти", "Sign out %(count)s selected devices|one": "Вийти з %(count)s вибраного пристрою", "Sign out %(count)s selected devices|other": "Вийти з %(count)s вибраних пристроїв", - "Sign out devices|one": "Вийти з притрою", + "Sign out devices|one": "Вийти з пристрою", "Sign out devices|other": "Вийти з пристроїв", "Click the button below to confirm signing out these devices.|other": "Клацніть кнопку внизу, щоб підтвердити вихід із цих пристроїв.", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Підтвердьте вихід із цих пристроїв за допомогою єдиного входу, щоб довести вашу справжність.", @@ -2374,7 +2374,7 @@ "Start a conversation with someone using their name or username (like ).": "Почніть розмову з кимось, ввівши їхнє ім'я чи користувацьке ім'я (вигляду ).", "Start a conversation with someone using their name, email address or username (like ).": "Почніть розмову з кимось, ввівши їхнє ім'я, е-пошту чи користувацьке ім'я (вигляду ).", "Suggestions": "Пропозиції", - "If you can't see who you're looking for, send them your invite link below.": "Якщо тут нема тих, кого шукаєте, надішліть їм запрошувальне посилання внизу.", + "If you can't see who you're looking for, send them your invite link below.": "Якщо тут немає тих, кого шукаєте, надішліть їм запрошувальне посилання внизу.", "Open dial pad": "Відкрити номеронабирач", "Dial pad": "Номеронабирач", "Only people invited will be able to find and join this space.": "Лише запрошені до цього простору люди зможуть знайти й приєднатися до нього.", @@ -2579,7 +2579,7 @@ "Mention": "Згадати", "Error removing address": "Помилка видалення адреси", "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Помилка видалення такої адреси. Можливо, вона не існує або стався тимчасовий збій.", - "You don't have permission to delete the address.": "У вас нема дозволу видаляти адресу.", + "You don't have permission to delete the address.": "У вас немає дозволу видаляти адресу.", "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Помилка створення такої адреси. Можливо, сервер цього не дозволяє або стався тимчасовий збій.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Помилка оновлення запасних адрес кімнати. Можливо, сервер цього не дозволяє або стався тимчасовий збій.", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Помилка оновлення головної адреси кімнати. Можливо, сервер цього не дозволяє або стався тимчасовий збій.", @@ -2588,7 +2588,7 @@ "Invited by %(sender)s": "Запрошення від %(sender)s", "Stickerpack": "Пакунок наліпок", "Add some now": "Додайте які-небудь", - "You don't currently have any stickerpacks enabled": "У вас поки нема пакунків наліпок", + "You don't currently have any stickerpacks enabled": "У вас поки немає пакунків наліпок", "%(roomName)s does not exist.": "%(roomName)s не існує.", "%(roomName)s can't be previewed. Do you want to join it?": "Попередній перегляд %(roomName)s недоступний. Бажаєте приєднатися?", "You're previewing %(roomName)s. Want to join it?": "Ви попередньо переглядаєте %(roomName)s. Бажаєте приєднатися?", @@ -2605,9 +2605,9 @@ "Historical": "Історичні", "System Alerts": "Системні попередження", "Explore public rooms": "Переглянути загальнодоступні кімнати", - "You do not have permissions to add rooms to this space": "У вас нема дозволу додавати кімнати до цього простору", - "You do not have permissions to create new rooms in this space": "У вас нема дозволу створювати кімнати в цьому просторі", - "No recently visited rooms": "Нема недавно відвіданих кімнат", + "You do not have permissions to add rooms to this space": "У вас немає дозволу додавати кімнати до цього простору", + "You do not have permissions to create new rooms in this space": "У вас немає дозволу створювати кімнати в цьому просторі", + "No recently visited rooms": "Немає недавно відвіданих кімнат", "Recently viewed": "Недавно переглянуті", "Close preview": "Закрити попередній перегляд", "Show %(count)s other previews|one": "Показати %(count)s інший попередній перегляд", @@ -2857,7 +2857,7 @@ "Failed to start livestream": "Не вдалося почати живу трансляцію", "See room timeline (devtools)": "Переглянути стрічку кімнати (розробка)", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Тут лише ви. Якщо ви вийдете, ніхто більше не зможе приєднатися, навіть ви самі.", - "You don't have permission to do this": "У вас нема на це дозволу", + "You don't have permission to do this": "У вас немає на це дозволу", "Message preview": "Попередній перегляд повідомлення", "%(hostSignupBrand)s Setup": "Налаштування %(hostSignupBrand)s", "You should know": "Варто знати", @@ -3098,7 +3098,7 @@ "Poll": "Опитування", "Voice Message": "Голосове повідомлення", "Hide stickers": "Сховати наліпки", - "You do not have permissions to add spaces to this space": "У вас нема дозволу додавати простори до цього простору", + "You do not have permissions to add spaces to this space": "У вас немає дозволу додавати простори до цього простору", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Дякуємо за випробування бета-версії. Просимо якнайдокладніше описати, що нам слід вдосконалити.", "How can I leave the beta?": "Як вимкнути бета-версію?", "Click for more info": "Натисніть, щоб дізнатися більше", @@ -3498,5 +3498,44 @@ "Last activity": "Остання активність", "Sessions": "Сеанси", "Use new session manager (under active development)": "Використовувати новий менеджер сеансів (в активній розробці)", - "Current session": "Поточний сеанс" + "Current session": "Поточний сеанс", + "Unverified": "Не звірений", + "Verified": "Звірений", + "Session details": "Подробиці сеансу", + "IP address": "IP-адреса", + "Device": "Пристрій", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "Для кращої безпеки звірте свої сеанси та вийдіть з усіх невикористовуваних або нерозпізнаних сеансів.", + "Other sessions": "Інші сеанси", + "Verify or sign out from this session for best security and reliability.": "Звірте цей сеанс або вийдіть із нього для поліпшення безпеки та надійності.", + "Unverified session": "Не звірений сеанс", + "This session is ready for secure messaging.": "Цей сеанс готовий для безпечного обміну повідомленнями.", + "Verified session": "Звірений сеанс", + "Inactive for %(inactiveAgeDays)s+ days": "Неактивний %(inactiveAgeDays)s+ днів", + "Welcome": "Вітаємо", + "Show shortcut to welcome checklist above the room list": "Показати ярлик контрольного списку привітання над списком кімнат", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Ви можете вийти зі старих сеансів (%(inactiveAgeDays)s днів або давніших), якими ви більше не користуєтеся", + "Inactive sessions": "Неактивні сеанси", + "View all": "Переглянути всі", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Звірте свої сеанси для покращеного безпечного обміну повідомленнями або вийдіть із тих, які ви не розпізнаєте або не використовуєте.", + "Unverified sessions": "Не звірені сеанси", + "Improve your account security by following these recommendations": "Удоскональте безпеку свого облікового запису, дотримуючись цих порад", + "Security recommendations": "Поради щодо безпеки", + "Filter devices": "Фільтрувати пристрої", + "Inactive for %(inactiveAgeDays)s days or longer": "Неактивний впродовж %(inactiveAgeDays)s днів чи довше", + "Inactive": "Неактивний", + "Not ready for secure messaging": "Не готовий до безпечного обміну повідомленнями", + "Ready for secure messaging": "Готовий до безпечного обміну повідомленнями", + "All": "Усі", + "No sessions found.": "Не знайдено сеансів.", + "No inactive sessions found.": "Не знайдено неактивних сеансів.", + "No unverified sessions found.": "Не знайдено не звірених сеансів.", + "No verified sessions found.": "Не знайдено звірених сеансів.", + "For best security, sign out from any session that you don't recognize or use anymore.": "Для кращої безпеки виходьте з усіх сеансів, які ви не розпізнаєте або більше не використовуєте.", + "Verified sessions": "Звірені сеанси", + "Interactively verify by emoji": "Звірити інтерактивно за допомогою емоджі", + "Manually verify by text": "Звірити вручну за допомогою тексту", + "Toggle device details": "Перемикнути відомості про пристрій", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "Ми будемо вдячні за відгук про %(brand)s.", + "How are you finding %(brand)s so far?": "Як вам %(brand)s?", + "Don’t miss a thing by taking %(brand)s with you": "Не пропускайте нічого, взявши з собою %(brand)s" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 50f04c4800f..93547acb240 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -21,7 +21,7 @@ "Failed to reject invitation": "拒绝邀请失败", "Failed to send email": "发送邮件失败", "Failed to send request.": "请求发送失败。", - "Failed to set display name": "设置昵称失败", + "Failed to set display name": "设置显示名称失败", "Failed to unban": "解除封禁失败", "Failed to verify email address: make sure you clicked the link in the email": "邮箱验证失败:请确保你已点击邮件中的链接", "Failure to create room": "创建房间失败", @@ -43,7 +43,7 @@ "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s 没有通知发送权限 - 请检查你的浏览器设置", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s 没有通知发送权限 - 请重试", "%(brand)s version:": "%(brand)s 版本:", - "Room %(roomId)s not visible": "房间 %(roomId)s 已隐藏", + "Room %(roomId)s not visible": "房间%(roomId)s不可见", "Rooms": "房间", "Search": "搜索", "Search failed": "搜索失败", @@ -76,7 +76,7 @@ "Continue": "继续", "Join Room": "加入房间", "Jump to first unread message.": "跳到第一条未读消息。", - "Leave room": "退出房间", + "Leave room": "离开房间", "Admin": "管理员", "Admin Tools": "管理员工具", "No Microphones detected": "未检测到麦克风", @@ -92,7 +92,7 @@ "and %(count)s others...|one": "和其它一个...", "Anyone": "任何人", "Are you sure?": "你确定吗?", - "Are you sure you want to leave the room '%(roomName)s'?": "你确定要退出房间 “%(roomName)s” 吗?", + "Are you sure you want to leave the room '%(roomName)s'?": "你确定要离开房间 “%(roomName)s” 吗?", "Are you sure you want to reject the invitation?": "你确定要拒绝邀请吗?", "Bans user with given id": "按照 ID 封禁用户", "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.": "无法连接主服务器 - 请检查网络连接,确保你的主服务器 SSL 证书被信任,且没有浏览器插件拦截请求。", @@ -101,13 +101,13 @@ "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s 将房间名称改为 %(roomName)s。", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s 移除了房间名称。", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s 将话题修改为 “%(topic)s”。", - "Changes your display nickname": "修改昵称", + "Changes your display nickname": "修改显示昵称", "Close": "关闭", "Command error": "命令错误", "Commands": "命令", "Custom level": "自定义级别", "Decline": "拒绝", - "Enter passphrase": "输入密语", + "Enter passphrase": "输入口令词组", "Export": "导出", "Failed to upload profile picture!": "用户资料图片上传失败!", "Home": "主页", @@ -115,10 +115,10 @@ "Incorrect username and/or password.": "用户名或密码错误。", "Invited": "已邀请", "Invites": "邀请", - "Invites user with given id to current room": "按照 ID 邀请指定用户加入当前房间", + "Invites user with given id to current room": "邀请指定ID的用户到当前房间", "Sign in with": "第三方登录", - "Missing room_id in request": "请求中没有 房间 ID", - "Missing user_id in request": "请求中没有 user_id", + "Missing room_id in request": "请求中缺少room_id", + "Missing user_id in request": "请求中缺少user_id", "Moderator": "协管员", "Mute": "静音", "Name": "名称", @@ -163,10 +163,10 @@ "Connectivity to the server has been lost.": "到服务器的连接已经丢失。", "New Password": "新密码", "Options": "选项", - "Passphrases must match": "密语必须匹配", - "Passphrase must not be empty": "密语不能为空", + "Passphrases must match": "口令词组必须匹配", + "Passphrase must not be empty": "口令词组不能为空", "Export room keys": "导出房间密钥", - "Confirm passphrase": "确认密语", + "Confirm passphrase": "确认口令词组", "Import room keys": "导入房间密钥", "File to import": "要导入的文件", "Failed to invite": "邀请失败", @@ -209,11 +209,11 @@ "Please check your email and click on the link it contains. Once this is done, click continue.": "请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s更改了%(powerLevelDiffText)s的权力级别。", "Deops user with given id": "按照 ID 取消特定用户的管理员权限", - "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s 设定历史浏览功能为 所有房间成员,从他们被邀请开始.", - "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s 设定历史浏览功能为 所有房间成员,从他们加入开始.", - "%(senderName)s made future room history visible to all room members.": "%(senderName)s 设定历史浏览功能为 所有房间成员.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s 设定历史浏览功能为 任何人.", - "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s 设定历史浏览功能为 未知的 (%(visibility)s).", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s使未来的房间历史对所有房间成员从他们被邀请开始可见。", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s使未来的房间历史对所有房间成员从他们加入开始可见。", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s使未来的房间历史对所有房间成员可见。", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s使未来的房间历史对任何人可见。", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s使未来的房间历史对未知(%(visibility)s)可见。", "AM": "上午", "PM": "下午", "Profile": "个人资料", @@ -221,11 +221,11 @@ "Start authentication": "开始认证", "This room is not recognised.": "无法识别此房间。", "Unable to add email address": "无法添加邮箱地址", - "Automatically replace plain text Emoji": "将符号表情转换为 Emoji", + "Automatically replace plain text Emoji": "自动取代纯文本Emoji", "Unable to verify email address.": "无法验证邮箱地址。", - "You do not have permission to do that in this room.": "你没有进行此操作的权限。", + "You do not have permission to do that in this room.": "你没有权限在此房间进行那个操作。", "You do not have permission to post to this room": "你没有在此房间发送消息的权限", - "You seem to be in a call, are you sure you want to quit?": "你似乎正在进行通话,确定要退出吗?", + "You seem to be in a call, are you sure you want to quit?": "你似乎正在通话,确定要退出吗?", "You seem to be uploading files, are you sure you want to quit?": "你似乎正在上传文件,确定要退出吗?", "Error decrypting image": "解密图像时出错", "Error decrypting video": "解密视频时出错", @@ -321,7 +321,7 @@ "%(items)s and %(count)s others|one": "%(items)s 与另一个人", "collapse": "折叠", "expand": "展开", - "Leave": "退出", + "Leave": "离开", "Description": "描述", "Warning": "警告", "Room Notification": "房间通知", @@ -341,7 +341,7 @@ "Failed to add tag %(tagName)s to room": "无法为房间新增标签 %(tagName)s", "Submit debug logs": "提交调试日志", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "你的电子邮件地址似乎没有同此主服务器上的Matrix ID绑定。", - "Restricted": "受限用户", + "Restricted": "受限", "Stickerpack": "贴纸包", "You don't currently have any stickerpacks enabled": "你目前未启用任何贴纸包", "Key request sent.": "已发送密钥共享请求。", @@ -363,18 +363,18 @@ "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s 已加入 %(count)s 次", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s 已加入", - "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已退出 %(count)s 次", - "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已退出", - "%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已退出 %(count)s 次", - "%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已退出", - "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s 已加入&已退出 %(count)s 次", - "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s 已加入&已退出", - "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s 已加入&已退出 %(count)s 次", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s 已加入&已退出", - "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s 退出并重新加入了 %(count)s 次", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s 退出并重新加入了", - "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s 退出并重新加入了 %(count)s 次", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s 退出并重新加入了", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已离开 %(count)s 次", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已离开", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已离开 %(count)s 次", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已离开", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s加入并离开了%(count)s次", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s加入并离开了", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s加入并离开了%(count)s次", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s加入并离开了", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s离开并重新加入了%(count)s次", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s离开并重新加入了", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s离开并重新加入了%(count)s次", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s离开并重新加入了", "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s 拒绝了他们的邀请", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s 拒绝了他们的邀请共 %(count)s 次", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s 拒绝了他们的邀请共 %(count)s 次", @@ -390,7 +390,7 @@ "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s 将房间的头像更改为 ", "Please enter the code it contains:": "请输入其包含的代码:", "Old cryptography data detected": "检测到旧的加密数据", - "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果你遇到问题,请退出并重新登录。要保留历史消息,请先导出并在重新登录后导入你的密钥。", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果你遇到问题,请登出并重新登录。要保留历史消息,请先导出并在重新登录后导入你的密钥。", "Uploading %(filename)s and %(count)s others|other": "正在上传 %(filename)s 与其他 %(count)s 个文件", "Uploading %(filename)s and %(count)s others|zero": "正在上传 %(filename)s", "Uploading %(filename)s and %(count)s others|one": "正在上传 %(filename)s 与其他 %(count)s 个文件", @@ -399,8 +399,8 @@ "Opens the Developer Tools dialog": "打开开发者工具窗口", "Notify the whole room": "通知房间全体成员", "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.": "此操作允许你将加密房间中收到的消息的密钥导出为本地文件。你可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。", - "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 passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,你应此小心对待,以确保其安全。为解决此问题,你应当在下面输入密语以加密导出的数据。只有输入相同的密语才能导入数据。", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "导出文件受密语保护。必须输入密语以解密此文件。", + "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 passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,你应此小心对待,以确保其安全。为解决此问题,你应当在下面输入口令词组以加密导出的数据。只有输入相同的口令词组才能导入数据。", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "导出文件受口令词组保护。你应该在此输入口令词组以解密此文件。", "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.": "此操作允许你导入之前从另一个 Matrix 客户端中导出的加密密钥文件。导入完成后,你将能够解密那个客户端可以解密的加密消息。", "Ignores a user, hiding their messages from you": "忽略用户,隐藏他们发送的消息", "Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息", @@ -419,7 +419,7 @@ "Failed to send logs: ": "无法发送日志: ", "This Room": "此房间", "Noisy": "响铃", - "Messages containing my display name": "当消息包含我的昵称时", + "Messages containing my display name": "当消息包含我的显示名称时", "Messages in one-to-one chats": "私聊中的消息", "Unavailable": "无法获得", "remove %(name)s from the directory.": "从目录中移除 %(name)s。", @@ -475,7 +475,7 @@ "Thank you!": "谢谢!", "Checking for an update...": "正在检查更新……", "You need to be able to invite users to do that.": "你需要有邀请用户的权限才能进行此操作。", - "Missing roomId.": "找不到此房间 ID 所对应的房间。", + "Missing roomId.": "缺少roomId。", "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?": "你将被带到一个第三方网站以便验证你的账户来使用 %(integrationsUrl)s 提供的集成。你希望继续吗?", "Popout widget": "在弹出式窗口中打开挂件", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "无法加载被回复的事件,它可能不存在,也可能是你没有权限查看它。", @@ -494,7 +494,7 @@ "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预览,以确保主服务器(生成预览的地方)无法获知你在此房间中看到的链接的有关的信息。", "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.": "当有人发送一条带有链接的消息后,可显示链接的预览,链接预览可包含此链接的网页标题、描述以及图片。", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "你确定要移除(删除)此事件吗?注意,如果删除房间名称或话题的更改,更改会被撤销。", - "Clear Storage and Sign Out": "清除数据并退出登录", + "Clear Storage and Sign Out": "清除存储并登出", "Send Logs": "发送日志", "Refresh": "刷新", "Share Room Message": "分享房间消息", @@ -608,7 +608,7 @@ "Enable Emoji suggestions while typing": "启用实时表情符号建议", "Show a placeholder for removed messages": "已移除的消息显示为一个占位符", "Show avatar changes": "显示头像更改", - "Show display name changes": "显示昵称更改", + "Show display name changes": "显示显示名称更改", "Show read receipts sent by other users": "显示其他用户发送的已读回执", "Show avatars in user and room mentions": "在用户和房间提及中显示头像", "Enable big emoji in chat": "在聊天中启用大型表情符号", @@ -704,7 +704,7 @@ "Verification code": "验证码", "Phone Number": "电话号码", "Profile picture": "头像", - "Display Name": "昵称", + "Display Name": "显示名称", "Set a new account password...": "设置一个新的账户密码……", "Email addresses": "电子邮箱地址", "Phone numbers": "电话号码", @@ -720,7 +720,7 @@ "Bug reporting": "错误上报", "FAQ": "常见问题", "Versions": "版本", - "Preferences": "偏好设置", + "Preferences": "偏好", "Composer": "编辑器", "Timeline": "时间线", "Room list": "房间列表", @@ -869,7 +869,7 @@ "Click the button below to confirm adding this phone number.": "点击下面的按钮以确认添加此电话号码。", "The file '%(fileName)s' failed to upload.": "上传文件 ‘%(fileName)s’ 失败。", "The server does not support the room version specified.": "服务器不支持指定的房间版本。", - "Cancel entering passphrase?": "取消输入密语?", + "Cancel entering passphrase?": "取消输入口令词组?", "Setting up keys": "设置密钥", "Verify this session": "验证此会话", "Encryption upgrade available": "提供加密升级", @@ -954,7 +954,7 @@ "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "跟你的%(brand)s管理员确认你的配置不正确或重复的条目。", "Cannot reach identity server": "无法连接到身份服务器", "Joins room with given address": "使用指定地址加入房间", - "Are you sure you want to cancel entering passphrase?": "你确定要取消输入密语吗?", + "Are you sure you want to cancel entering passphrase?": "你确定要取消输入口令词组吗?", "Go Back": "后退", "Light": "浅色", "Dark": "深色", @@ -1062,7 +1062,7 @@ "Backup has an invalid signature from verified session ": "备份有一个无效的签名,它来自已验证的会话", "Backup has an invalid signature from unverified session ": "备份有一个无效的签名,它来自未验证的会话", "Backup is not signed by any of your sessions": "备份没有被你的任何一个会话签名", - "This backup is trusted because it has been restored on this session": "此备份是受信任的因为它被恢复到了此会话上", + "This backup is trusted because it has been restored on this session": "此备份是受信任的因为它恢复到了此会话上", "Your keys are not being backed up from this session.": "你的密钥没有被此会话备份。", "Clear notifications": "清除通知", "Enable desktop notifications for this session": "为此会话启用桌面通知", @@ -1138,7 +1138,7 @@ "Always show the window menu bar": "总是显示窗口菜单栏", "Session ID:": "会话 ID:", "Session key:": "会话密钥:", - "Message search": "信息搜索", + "Message search": "消息搜索", "Cross-signing": "交叉签名", "View older messages in %(roomName)s.": "查看 %(roomName)s 里更旧的信息。", "Uploaded sound": "已上传的声音", @@ -1186,7 +1186,7 @@ "This bridge is managed by .": "此桥接由 管理。", "Homeserver feature support:": "主服务器功能支持:", "Securely cache encrypted messages locally for them to appear in search results.": "在本地安全地缓存加密消息以使其出现在搜索结果中。", - "Connecting to integration manager...": "正在连接至集成管理器...", + "Connecting to integration manager...": "正在连接至集成管理器……", "Cannot connect to integration manager": "不能连接到集成管理器", "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问你的主服务器。", "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查你的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", @@ -1297,7 +1297,7 @@ "Waiting for %(displayName)s to accept…": "正在等待%(displayName)s接受……", "Accepting…": "正在接受……", "Start Verification": "开始验证", - "Messages in this room are end-to-end encrypted.": "此房间内的消息是端对端加密的。", + "Messages in this room are end-to-end encrypted.": "此房间内的消息端到端加密。", "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "你的消息是安全的,只有你和接收者有解开它们的唯一密钥。", "Messages in this room are not end-to-end encrypted.": "此房间内的消息未端对端加密。", "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "在加密房间中,你的消息是安全的,只有你和接收者有解开它们的唯一密钥。", @@ -1614,7 +1614,7 @@ "Restore": "恢复", "You'll need to authenticate with the server to confirm the upgrade.": "你需要和服务器进行认证以确认更新。", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "更新此会话以允许其验证其他会话、允许其他会话访问加密消息,并将它们对别的用户标记为已信任。", - "Use a different passphrase?": "使用不同的密语?", + "Use a different passphrase?": "使用不同的口令词组?", "Copy": "复制", "Unable to query secret storage status": "无法查询秘密存储状态", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "如果你现在取消,你可能会丢失加密的消息和数据,如果你丢失了登录信息的话。", @@ -1914,7 +1914,7 @@ "Send message": "发送消息", "Invite to this space": "邀请至此空间", "Your message was sent": "消息已发送", - "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 Security Key.": "请使用你的账户数据备份加密密钥,以免你无法访问你的会话。密钥将通过一个唯一的安全密钥进行保护。", + "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 Security Key.": "请使用你的账户数据备份加密密钥,以免你无法访问你的会话。密钥会由一个唯一安全密钥保护。", "Spell check dictionaries": "拼写检查字典", "Failed to save your profile": "个人资料保存失败", "The operation could not be completed": "操作无法完成", @@ -2091,7 +2091,7 @@ "See %(eventType)s events posted to this room": "查看此房间中发送的 %(eventType)s 事件", "See %(eventType)s events posted to your active room": "查看你的活跃房间中发送的 %(eventType)s 事件", "Send %(eventType)s events as you in your active room": "以你的身份在你的活跃房间发送%(eventType)s事件", - "Send %(eventType)s events as you in this room": "使用当前账号在此房间发送 %(eventType)s 事件", + "Send %(eventType)s events as you in this room": "以你的身份在此房间发送 %(eventType)s 事件", "with an empty state key": "附带一个空的状态键(state key)", "See when a sticker is posted in this room": "查看此房间中何时有人发送贴纸", "See when the avatar changes in your active room": "查看你的活跃房间的头像何时修改", @@ -2189,7 +2189,7 @@ "Malta": "马耳他", "Mali": "马里", "Ignored attempt to disable encryption": "已忽略禁用加密的尝试", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "此房间中的消息已被端对端加密。当人们加入,你可以点击他们的头像,在他们的资料中验证他们。", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "此房间的消息端到端加密。当人们加入时,你可以点击他们的头像,在他们的用户资料中验证他们。", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "此处的消息已被端对端加密。请点击对方头像,在其资料中验证 %(displayName)s。", "Secure your backup with a Security Phrase": "使用安全短语保护你的备份", "Confirm your Security Phrase": "确认你的安全短语", @@ -2313,7 +2313,7 @@ "Invite someone using their name, email address, username (like ) or share this room.": "使用名字、电子邮件地址、用户名(如)邀请某人或分享此房间。", "Invite someone using their name, username (like ) or share this space.": "使用某人的名字、用户名(如 )邀请他们,或分享此空间。", "Invite someone using their name, email address, username (like ) or share this space.": "使用某人的名字、电子邮箱地址或用户名(如 )邀请他们,或分享此空间。", - "Start a conversation with someone using their name or username (like ).": "使用某人的名字或用户名开始与其进行对话(如 )。", + "Start a conversation with someone using their name or username (like ).": "使用某人的名字或用户名(如 )开始与其进行对话。", "Start a conversation with someone using their name, email address or username (like ).": "使用某人的名称、电子邮箱地址或用户名来与其开始对话(如 )。", "A call can only be transferred to a single user.": "通话只能转移到单个用户。", "We couldn't create your DM.": "我们无法创建你的私聊。", @@ -2328,7 +2328,7 @@ "Search names and descriptions": "搜索名称和描述", "You may want to try a different search or check for typos.": "你可能要尝试其他搜索或检查是否有错别字。", "You may contact me if you have any follow up questions": "如果你有任何后续问题,可以联系我", - "To leave the beta, visit your settings.": "要退出neta,请访问你的设置。", + "To leave the beta, visit your settings.": "要离开beta,请访问你的设置。", "Your platform and username will be noted to help us use your feedback as much as we can.": "我们将会记录你的平台及用户名,以帮助我们尽我们所能地使用你的反馈。", "Want to add a new room instead?": "想要添加一个新的房间吗?", "Add existing rooms": "添加现有房间", @@ -2360,7 +2360,7 @@ "You do not have permissions to add rooms to this space": "你没有权限添加房间至此空间", "You do not have permissions to create new rooms in this space": "你没有权限在此空间内创建新的房间", "Invite to just this room": "仅邀请至此房间", - "This is the beginning of your direct message history with .": "这是你与 间进行私聊的历史记录的开始。", + "This is the beginning of your direct message history with .": "这是你与的私聊历史的开端。", "Only the two of you are in this conversation, unless either of you invites anyone to join.": "除非你们其中一个邀请了别人加入,否则将仅有你们两个人在此对话中。", "%(seconds)ss left": "剩余 %(seconds)s 秒", "Failed to send": "发送失败", @@ -2368,11 +2368,11 @@ "You have no ignored users.": "你没有设置忽略用户。", "Warn before quitting": "退出前警告", "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "想要来点实验?实验室是提前体验、测试新功能并在它们正式发布前帮助它们定型的最佳方式。了解更多。", - "Your access token gives full access to your account. Do not share it with anyone.": "你的访问令牌可以完全访问你的帐户。不要将其与任何人分享。", + "Your access token gives full access to your account. Do not share it with anyone.": "你的访问令牌可以完全访问你的账户。不要将其与任何人分享。", "Access Token": "访问令牌", "Message search initialisation failed": "消息搜索初始化失败", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 来存储来自 %(rooms)s 房间的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 来存储来自 %(rooms)s 房间的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用%(size)s存储%(rooms)s个房间的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用%(size)s存储%(rooms)s个房间的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", "Manage & explore rooms": "管理并探索房间", "Please enter a name for the space": "请输入空间名称", "Play": "播放", @@ -2404,7 +2404,7 @@ "Something went wrong in confirming your identity. Cancel and try again.": "确认你的身份时出了一点问题。取消并重试。", "Avatar": "头像", "Join the beta": "加入beta", - "Leave the beta": "退出beta", + "Leave the beta": "离开beta", "Beta": "beta", "Start audio stream": "开始音频流", "Failed to start livestream": "开始流直播失败", @@ -2526,16 +2526,16 @@ "%(senderName)s set a profile picture": "%(senderName)s 已设置资料图片", "%(senderName)s changed their profile picture": "%(senderName)s 已更改他们的资料图片", "%(senderName)s removed their profile picture": "%(senderName)s 已移除他们的资料图片", - "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s 已将他们的昵称移除(%(oldDisplayName)s)", - "%(senderName)s set their display name to %(displayName)s": "%(senderName)s 已将他们的昵称设置为 %(displayName)s", + "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s已移除他们的显示名称(%(oldDisplayName)s)", + "%(senderName)s set their display name to %(displayName)s": "%(senderName)s已将他们的显示名称设置为%(displayName)s", "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s将其显示名称改为%(displayName)s", "%(senderName)s banned %(targetName)s": "%(senderName)s 已封禁 %(targetName)s", "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s 已封禁 %(targetName)s: %(reason)s", "%(senderName)s invited %(targetName)s": "%(senderName)s 已邀请 %(targetName)s", "%(targetName)s accepted an invitation": "%(targetName)s 已接受邀请", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s 已接受 %(displayName)s 的邀请", - "Some invites couldn't be sent": "部分邀请无法送达", - "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,除了以下无法邀请至 的人", + "Some invites couldn't be sent": "部分邀请无法发送", + "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,但无法邀请以下人员至", "Integration manager": "集成管理器", "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作,请联系管理员。", "Using this widget may share data with %(widgetDomain)s & your integration manager.": "使用此挂件可能会和 %(widgetDomain)s 及您的集成管理器共享数据 。", @@ -2562,9 +2562,9 @@ "New keyword": "新的关键词", "Keyword": "关键词", "Enable email notifications for %(email)s": "为 %(email)s 启用电子邮件通知", - "Enable for this account": "为此帐号启用", - "An error occurred whilst saving your notification preferences.": "保存你的通知首选项时出错。", - "Error saving notification preferences": "保存通知设置时出错", + "Enable for this account": "为此账户启用", + "An error occurred whilst saving your notification preferences.": "保存你的通知偏好时出错。", + "Error saving notification preferences": "保存通知偏好时出错", "Messages containing keywords": "当消息包含关键词时", "Message bubbles": "消息气泡", "Show all rooms": "显示所有房间", @@ -2659,7 +2659,7 @@ "Show %(count)s other previews|one": "显示 %(count)s 个其他预览", "Show %(count)s other previews|other": "显示 %(count)s 个其他预览", "Access": "访问", - "People with supported clients will be able to join the room without having a registered account.": "拥有受支持客户端的人无需注册帐号即可加入房间。", + "People with supported clients will be able to join the room without having a registered account.": "拥有受支持客户端的人无需注册账户即可加入房间。", "Decide who can join %(roomName)s.": "决定谁可以加入 %(roomName)s。", "Space members": "空间成员", "Anyone in a space can find and join. You can select multiple spaces.": "空间中的任何人都可以找到并加入。你可以选择多个空间。", @@ -2803,12 +2803,12 @@ "Automatically send debug logs on any error": "遇到任何错误自动发送调试日志", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "在下方管理您已登录的设备。 与您交流的人可以看到设备的名称。", "Rename": "重命名", - "Sign Out": "退出", + "Sign Out": "登出", "Last seen %(date)s at %(ip)s": "上次见到日期 %(date)s, IP %(ip)s", "This device": "此设备", "You aren't signed into any other devices.": "您没有登录任何其他设备。", - "Sign out %(count)s selected devices|one": "退出 %(count)s 台选定的设备", - "Sign out %(count)s selected devices|other": "退出 %(count)s 台选定的设备", + "Sign out %(count)s selected devices|one": "登出%(count)s台选定的设备", + "Sign out %(count)s selected devices|other": "登出%(count)s台选定的设备", "Devices without encryption support": "不支持加密的设备", "Unverified devices": "未验证的设备", "Verified devices": "已验证的设备", @@ -2816,21 +2816,21 @@ "Deselect all": "取消全选", "Sign out devices|one": "注销设备", "Sign out devices|other": "注销设备", - "Click the button below to confirm signing out these devices.|one": "单击下面的按钮以确认退出此设备。", - "Click the button below to confirm signing out these devices.|other": "单击下面的按钮以确认退出这些设备。", + "Click the button below to confirm signing out these devices.|one": "单击下面的按钮以确认登出此设备。", + "Click the button below to confirm signing out these devices.|other": "单击下面的按钮以确认登出这些设备。", "Confirm signing out these devices": "确认退出这些设备", "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "通过使用单点登录来证明您的身份,确认注销此设备。", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "通过使用单点登录来证明您的身份,确认注销这些设备。", "Unable to load device list": "无法加载设备列表", "Your homeserver does not support device management.": "您的主服务器不支持设备管理。", "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.": "将您的安全密钥存放在安全的地方,例如密码管理器或保险箱,因为它用于保护您的加密数据。", - "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 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.": "输入只有您知道的安全短语,因为它用于保护您的数据。 为了安全起见,您不应重复使用您的账户密码。", "We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "我们将为您生成一个安全密钥,将其存储在安全的地方,例如密码管理器或保险箱。", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.": "重新获取帐户访问权限并恢复存储在此会话中的加密密钥。 没有它们,您将无法在任何会话中阅读所有安全消息。", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.": "重新获取账户访问权限并恢复存储在此会话中的加密密钥。 没有它们,您将无法在任何会话中阅读所有安全消息。", "Without verifying, you won't have access to all your messages and may appear as untrusted to others.": "如果不进行验证,您将无法访问您的所有消息,并且在其他人看来可能不受信任。", "Shows all threads you've participated in": "显示您参与的所有消息列", "You're all caught up": "一切完毕", - "We call the places where you can host your account 'homeservers'.": "我们将您可以托管帐户的地方称为“主服务器”。", + "We call the places where you can host your account 'homeservers'.": "我们将您可以托管账户的地方称为“主服务器”。", "Matrix.org is the biggest public homeserver in the world, so it's a good place for many.": "Matrix.org 是世界上最大的公共主服务器,因此对许多人来说是一个好地方。", "If you can't see who you're looking for, send them your invite link below.": "如果您看不到您要找的人,请将您的邀请链接发送给他们。", "You can't disable this later. Bridges & most bots won't work yet.": "之后你无法停用。桥接和大多数机器人也不能工作。", @@ -2868,7 +2868,7 @@ "Keep discussions organised with threads": "用消息列使讨论井然有序", "Rooms outside of a space": "空间之外的房间", "Show all your rooms in Home, even if they're in a space.": "在主页展示你所有的房间,即使它们是在一个空间里。", - "Home is useful for getting an overview of everything.": "对于了解所有事情的概况来说,主页很有用的。", + "Home is useful for getting an overview of everything.": "对于了解所有事情的概况来说,主页很有用。", "Manage rooms in this space": "管理此空间中的房间", "Copy link": "复制链接", "Mentions only": "仅提及", @@ -2924,7 +2924,7 @@ "No votes cast": "尚无投票", "You can turn this off anytime in settings": "您可以随时在设置中关闭此功能", "We don't share information with third parties": "我们不会与第三方共享信息", - "We don't record or profile any account data": "我们不会记录或配置任何帐户数据", + "We don't record or profile any account data": "我们不会记录或配置任何账户数据", "You can read all our terms here": "你可以在此处阅读我们所有的条款", "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "共享匿名数据以帮助我们发现问题。 与个人无关。 没有第三方。", "Okay": "好", @@ -3030,7 +3030,7 @@ "Unable to find Matrix ID for phone number": "未能找到与此手机号码关联的 Matrix ID", "No virtual room for this room": "此房间未有虚拟房间", "Switches to this room's virtual room, if it has one": "切换到此房间的虚拟房间(如有)", - "Developer command: Discards the current outbound group session and sets up new Olm sessions": "开发者命令:放弃当前输出群组绘画并设置新的 Olm 会话", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "开发者命令:放弃当前输出群组会话并设置新的Olm会话", "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)": "未知用户会话配对:(%(userId)s:%(deviceId)s)", "Command failed: Unable to find room (%(roomId)s": "命令失败:无法找到房间(%(roomId)s)", "Removes user with given id from this room": "将给定 ID 的用户移除此房间", @@ -3071,7 +3071,7 @@ "Room ID: %(roomId)s": "房间ID: %(roomId)s", "Are you sure you're at the right place?": "你确定你位于正确的地方?", "Show Labs settings": "显示实验室设置", - "Show HTML representation of room topics": "以HTML的表现形式显示房间标题", + "Show HTML representation of room topics": "显示房间话题的HTML表现形式", "Show join/leave messages (invites/removes/bans unaffected)": "显示加入/退出消息 (邀请/删除/踢出 不受影响)", "Show current avatar and name for users in message history": "在消息历史中显示当前用户使用的头像和名字", "Add new server…": "添加新的服务器…", @@ -3118,10 +3118,10 @@ "Group all your people in one place.": "将你所有的联系人集中一处。", "Group all your favourite rooms and people in one place.": "将所有你最爱的房间和人集中在一处。", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "空间是将房间和人分组的方式。除了你所在的空间,你也可以使用预建的空间。", - "Enable hardware acceleration (restart %(appName)s to take effect)": "启用硬件加速(重启%(appName)s以使其生效)", + "Enable hardware acceleration (restart %(appName)s to take effect)": "启用硬件加速(重启%(appName)s生效)", "Keyboard": "键盘", - "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.": "debug日志包含应用使用数据,其中包括你的用户名、你访问过的房间的别名或ID、你上次与哪些UI元素互动、还有其它用户的用户名。但不包含消息。", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "若你通过GitHub提交bug,则debug日志能帮助我们追踪问题。 ", + "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.": "调试日志包含应用使用数据,其中包括你的用户名、你访问过的房间的别名或ID、你上次与哪些UI元素互动、还有其它用户的用户名。但不包含消息。", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "若你通过GitHub提交bug,则调试日志能帮助我们追踪问题。 ", "Deactivating your account is a permanent action — be careful!": "停用你的账户是永久性动作——小心!", "You will not receive push notifications on other devices until you sign back in to them.": "在你重新登录其他设备之前,你将不会在这些设备上收到推送通知。", "Your password was successfully changed.": "你的密码已成功更改。", @@ -3155,7 +3155,7 @@ "Enable hardware acceleration": "启用硬件加速", "Automatically send debug logs when key backup is not functioning": "当密钥备份无法运作时自动发送debug日志", "Automatically send debug logs on decryption errors": "自动发送有关解密错误的debug日志", - "Start messages with /plain to send without markdown and /md to send with.": "信息以/plain为开头则不会使用markdown,以/md开头则会使用。", + "Start messages with /plain to send without markdown and /md to send with.": "消息以/plain为开头则不会使用markdown,以/md开头则会使用。", "Enable Markdown": "启用Markdown", "Insert a trailing colon after user mentions at the start of a message": "在消息开头的提及用户的地方后面插入尾随冒号", "Show polls button": "显示投票按钮", @@ -3171,7 +3171,7 @@ "How can I leave the beta?": "我如何离开beta?", "Use “%(replyInThread)s” when hovering over a message.": "用“%(replyInThread)s”,当悬停在一条消息上时。", "How can I start a thread?": "我如何发起消息列?", - "Threads help keep conversations on-topic and easy to track. Learn more.": "消息列帮助保持对话不离题且易于跟踪。了解更多。", + "Threads help keep conversations on-topic and easy to track. Learn more.": "消息列帮助保持对话切题且易于跟踪。了解更多。", "Keep discussions organised with threads.": "用消息列保持讨论的条理性。", "Explore public spaces in the new search dialog": "在新的搜索对话框中探索公开空间", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "感谢你试用beta版,请尽可能详细地说明,以便我们能够改进它。", @@ -3182,7 +3182,7 @@ "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "视频房间是嵌入在%(brand)s房间内的总是开启的VoIP频道。", "Join the room to participate": "加入房间以参与", "Tip: Use “%(replyInThread)s” when hovering over a message.": "实用提示:悬停在消息上时使用“%(replyInThread)s”。", - "Threads help keep your conversations on-topic and easy to track.": "消息列帮助保持你的对话不离题并易于追踪。", + "Threads help keep your conversations on-topic and easy to track.": "消息列帮助保持你的对话切题并易于追踪。", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "回复进行中的消息列或当悬停在消息上时使用%(replyInThread)s来发起新的消息列。", "Can't create a thread from an event with an existing relation": "无法从既有关系的事件创建消息列", "Threads are a beta feature": "消息列是beta功能", @@ -3368,5 +3368,119 @@ "Download on the App Store": "在App Store下载", "Send read receipts": "发送已读回执", "Share your activity and status with others.": "与别人分享你的活动和状态。", - "Your server doesn't support disabling sending read receipts.": "你的服务器不支持禁用发送已读回执。" + "Your server doesn't support disabling sending read receipts.": "你的服务器不支持禁用发送已读回执。", + "Complete these to get the most out of %(brand)s": "完成这些步骤以充分利用%(brand)s", + "iOS": "iOS", + "Android": "Android", + "We're creating a room with %(names)s": "正在创建房间%(names)s", + "Use new session manager (under active development)": "使用新的会话管理器(正在积极开发)", + "Sessions": "会话", + "Current session": "当前会话", + "Verified": "已验证", + "Unverified": "未验证", + "Verified session": "已验证的会话", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "为了最佳的安全,请验证会话,登出任何不认识或不再使用的会话。", + "Other sessions": "其他会话", + "Welcome": "欢迎", + "Show shortcut to welcome checklist above the room list": "在房间列表上方显示欢迎清单的捷径", + "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)s移除了1条消息", + "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)s移除了%(count)s条消息", + "Remove them from everything I'm able to": "", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "请考虑从不再使用的旧会话(%(inactiveAgeDays)s天或更久)登出", + "Inactive sessions": "不活跃的会话", + "View all": "查看全部", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "验证你的会话以增强消息传输的安全性,或从那些你不认识或不再使用的会话登出。", + "Unverified sessions": "未验证的会话", + "Improve your account security by following these recommendations": "按照下面的推荐改善账户安全", + "Security recommendations": "安全建议", + "Inactive for %(inactiveAgeDays)s+ days": "%(inactiveAgeDays)s+天不活跃", + "Session details": "会话详情", + "IP address": "IP地址", + "Device": "设备", + "Last activity": "上次活动", + "Verify or sign out from this session for best security and reliability.": "验证此会话或从之登出,以取得最佳安全性和可靠性。", + "Unverified session": "未验证的会话", + "This session is ready for secure messaging.": "此会话已准备好进行安全的消息传输。", + "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)s发送了%(count)s条隐藏消息", + "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)s发送了一条隐藏消息", + "Remove server “%(roomServer)s”": "移除服务器“%(roomServer)s”", + "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.": "你可以使用自定义服务器选项来指定不同的主服务器URL以登录其他Matrix服务器。这让你能把%(brand)s和不同主服务器上的已有Matrix账户搭配使用。", + "Started": "已开始", + "Requested": "已请求", + "Unsent": "未发送", + "Edit values": "编辑值", + "Failed to save settings.": "保存设置失败。", + "Number of users": "用户数", + "Server": "服务器", + "Server Versions": "服务器版本", + "Send custom account data event": "发送自定义账户数据事件", + "Search Dialog": "搜索对话", + "Join %(roomAddress)s": "加入%(roomAddress)s", + "%(count)s Members|other": "%(count)s个成员", + "Ignore user": "忽略用户", + "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.": "当你登出时,这些密钥会从此设备删除。这意味着你将无法查阅已加密消息,除非你在其他设备上有那些消息的密钥,或者已将其备份到服务器。", + "Open room": "打开房间", + "Export Cancelled": "导出已取消", + "Server info": "服务器信息", + "Output devices": "输出设备", + "Input devices": "输入设备", + "No verification requests found": "未找到验证请求", + "Observe only": "仅观察", + "Requester": "请求者", + "Methods": "方法", + "Timeout": "超时", + "Phase": "阶段", + "Transaction": "交易", + "Cancelled": "已取消", + "View List": "查看列表", + "View list": "查看列表", + "No live locations": "无实时位置", + "Live location error": "实时位置错误", + "Live location ended": "实时位置已结束", + "Loading live location...": "正在加载实时位置……", + "Live until %(expiryTime)s": "实时分享直至%(expiryTime)s", + "View related event": "查看相关事件", + "Cameras": "相机", + "Unread email icon": "未读电子邮件图标", + "Check your email to continue": "检查你的电子邮件以继续", + "An error occurred while stopping your live location, please try again": "停止你的实时位置时出错,请重试", + "An error occurred whilst sharing your live location, please try again": "分享你的实时位置时出错,请重试", + "Live location enabled": "实时位置已启用", + "%(timeRemaining)s left": "剩余%(timeRemaining)s", + "You are sharing your live location": "你正在分享你的实时位置", + "An error occurred whilst sharing your live location": "分享实时位置时出错", + "An error occurred while stopping your live location": "停止实时位置时出错", + "Close sidebar": "关闭侧边栏", + "Navigate up in the room list": "在房间列表中向上导航", + "Navigate down in the room list": "在房间列表中向下导航", + "Toggle Code Block": "切换代码块", + "Failed to set direct message tag": "设置私聊标签失败", + "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.": "你已登出全部设备,并将不再收到推送通知。要重新启用通知,请在每台设备上再次登入。", + "Sign out all devices": "登出全部设备", + "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.": "若想保留对加密房间的聊天历史的访问权,请设置密钥备份或从其他设备导出消息密钥,然后再继续。", + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "登出你的设备会删除存储在其上的消息加密密钥,使加密的聊天历史不可读。", + "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "重置你在这个主服务器上的密码将导致你全部设备登出。这会删除存储在其上的消息加密密钥,使加密的聊天历史不可读。", + "Event ID: %(eventId)s": "事件ID:%(eventId)s", + "Resent!": "已重新发送!", + "Did not receive it? Resend it": "没收到吗?重新发送", + "To create your account, open the link in the email we just sent to %(emailAddress)s.": "要创建账户,请打开我们刚刚发送到%(emailAddress)s的电子邮件里的链接。", + "Toggle Link": "切换链接", + "Previous recently visited room or space": "上一个最近访问过的房间或空间", + "Next recently visited room or space": "下一个最近访问过的房间或空间", + "Open user settings": "打开用户设置", + "Verified sessions": "已验证的会话", + "For best security, sign out from any session that you don't recognize or use anymore.": "为了最佳安全性,请从任何不认识或不再使用的会话登出。", + "No verified sessions found.": "未找到已验证的会话。", + "No unverified sessions found.": "未找到未验证的会话。", + "No inactive sessions found.": "未找到不活跃的会话。", + "No sessions found.": "未找到会话。", + "All": "全部", + "Ready for secure messaging": "准备好进行安全通信了", + "Not ready for secure messaging": "尚未准备好安全通信", + "Inactive": "不活跃", + "Inactive for %(inactiveAgeDays)s days or longer": "%(inactiveAgeDays)s天或更久不活跃", + "Filter devices": "筛选设备", + "Toggle device details": "切换设备详情", + "Manually verify by text": "用文本手动验证", + "Interactively verify by emoji": "用emoji交互式验证" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index e17feb2b9a9..88c143809d4 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3205,7 +3205,7 @@ "Started": "已開始", "Ready": "準備", "Requested": "已請求", - "Unsent": "取消傳送", + "Unsent": "未傳送", "Edit values": "編輯值", "Failed to save settings.": "儲存設定失敗。", "Number of users": "使用者數量", @@ -3497,5 +3497,42 @@ "Send read receipts": "傳送讀取回條", "Last activity": "上次活動", "Sessions": "工作階段", - "Use new session manager (under active development)": "使用新的工作階段管理程式(正在積極開發中)" + "Use new session manager (under active development)": "使用新的工作階段管理程式(正在積極開發中)", + "Current session": "目前的工作階段", + "Unverified": "未驗證", + "Verified": "已驗證", + "Inactive for %(inactiveAgeDays)s+ days": "閒置 %(inactiveAgeDays)s+ 天", + "Session details": "工作階段詳細資料", + "IP address": "IP 地址", + "Device": "裝置", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "為了最佳的安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。", + "Other sessions": "其他工作階段", + "Verify or sign out from this session for best security and reliability.": "驗證或登出此工作階段以取得最佳安全性與可靠程度。", + "Unverified session": "未經驗證的工作階段", + "This session is ready for secure messaging.": "此工作階段已準備好進行安全通訊。", + "Verified session": "已驗證的工作階段", + "Welcome": "歡迎", + "Show shortcut to welcome checklist above the room list": "在聊天室清單上方顯示歡迎清單的捷徑", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "考慮登出您不再使用的舊工作階段(%(inactiveAgeDays)s天或更舊)", + "Inactive sessions": "不活躍的工作階段", + "View all": "檢視全部", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "驗證您的工作階段以強化安全通訊,或從您無法識別或不再使用的工作階段登出。", + "Unverified sessions": "未驗證的工作階段", + "Improve your account security by following these recommendations": "按照這些建議提昇您的帳號安全性", + "Security recommendations": "安全建議", + "Filter devices": "過濾裝置", + "Inactive for %(inactiveAgeDays)s days or longer": "不活躍 %(inactiveAgeDays)s 天或更久", + "Inactive": "非作用中", + "Not ready for secure messaging": "尚未準備好安全通訊", + "Ready for secure messaging": "準備好進行安全通訊", + "All": "全部", + "No sessions found.": "找不到工作階段。", + "No inactive sessions found.": "找不到非作用中的工作階段。", + "No unverified sessions found.": "找不到未驗證的工作階段。", + "No verified sessions found.": "找不到已驗證的工作階段。", + "For best security, sign out from any session that you don't recognize or use anymore.": "為了取得最佳安全性,請從任何您無法識別或不再使用的工作階段登出。", + "Verified sessions": "已驗證的工作階段", + "Interactively verify by emoji": "透過表情符號互動式驗證", + "Manually verify by text": "透過文字手動驗證", + "Toggle device details": "切換裝置詳細資訊" } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0e0131b0bb6..01f3457119e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -422,13 +422,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Send read receipts"), default: true, }, - "feature_message_right_click_context_menu": { - isFeature: true, - supportedLevels: LEVELS_FEATURE, - labsGroup: LabGroup.Rooms, - displayName: _td("Right-click message context menu"), - default: true, - }, "feature_location_share_live": { isFeature: true, labsGroup: LabGroup.Messaging, @@ -868,6 +861,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: true, controller: new IncompatibleController("feature_breadcrumbs_v2", true), }, + "FTUE.userOnboardingButton": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Show shortcut to welcome checklist above the room list"), + default: true, + }, "showHiddenEventsInTimeline": { displayName: _td("Show hidden events in timeline"), supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, diff --git a/src/settings/controllers/SettingController.ts b/src/settings/controllers/SettingController.ts index a274bcff2c3..2d747e52930 100644 --- a/src/settings/controllers/SettingController.ts +++ b/src/settings/controllers/SettingController.ts @@ -63,16 +63,14 @@ export default abstract class SettingController { * @param {String} roomId The room ID, may be null. * @param {*} newValue The new value for the setting, may be null. */ - public onChange(level: SettingLevel, roomId: string, newValue: any) { + public onChange(level: SettingLevel, roomId: string, newValue: any): void { // do nothing by default - - // FIXME: force a fresh on the RoomView for the roomId in question } /** * Gets whether the setting has been disabled due to this controller. */ - public get settingDisabled() { + public get settingDisabled(): boolean { return false; } } diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts index d1a2c6c6224..f7a5fe9ca5f 100644 --- a/src/settings/handlers/AccountSettingsHandler.ts +++ b/src/settings/handlers/AccountSettingsHandler.ts @@ -165,8 +165,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa content[field] = value; - await this.client.setAccountData(eventType, content); - + // Attach a deferred *before* setting the account data to ensure we catch any requests + // which race between different lines. const deferred = defer(); const handler = (event: MatrixEvent) => { if (event.getType() !== eventType || event.getContent()[field] !== value) return; @@ -175,6 +175,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa }; this.client.on(ClientEvent.AccountData, handler); + await this.client.setAccountData(eventType, content); + await deferred.promise; } diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index ea2b7e93ad2..4f0e7d5b13b 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -24,7 +24,6 @@ import { ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/ty import { JoinedRoom as JoinedRoomEvent } from "@matrix-org/analytics-events/types/typescript/JoinedRoom"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { Room } from "matrix-js-sdk/src/models/room"; -import { ClientEvent } from "matrix-js-sdk/src/client"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; @@ -48,6 +47,7 @@ import { JoinRoomErrorPayload } from "../dispatcher/payloads/JoinRoomErrorPayloa import { ViewRoomErrorPayload } from "../dispatcher/payloads/ViewRoomErrorPayload"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload"; +import { awaitRoomDownSync } from "../utils/RoomUpgrade"; const NUM_JOIN_RETRY = 5; @@ -209,10 +209,7 @@ export class RoomViewStore extends Store { this.setState({ shouldPeek: false }); } - const cli = MatrixClientPeg.get(); - - const updateMetrics = () => { - const room = cli.getRoom(payload.roomId); + awaitRoomDownSync(MatrixClientPeg.get(), payload.roomId).then(room => { const numMembers = room.getJoinedMemberCount(); const roomSize = numMembers > 1000 ? "MoreThanAThousand" : numMembers > 100 ? "OneHundredAndOneToAThousand" @@ -228,15 +225,7 @@ export class RoomViewStore extends Store { isDM: !!DMRoomMap.shared().getUserIdForRoomId(room.roomId), isSpace: room.isSpaceRoom(), }); - - cli.off(ClientEvent.Room, updateMetrics); - }; - - if (cli.getRoom(payload.roomId)) { - updateMetrics(); - } else { - cli.on(ClientEvent.Room, updateMetrics); - } + }); break; } diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index db21e635b49..241530f77fc 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -21,6 +21,8 @@ import { arrayDiff } from "../../utils/arrays"; import { RoomNotificationState } from "./RoomNotificationState"; import { NotificationState, NotificationStateEvents } from "./NotificationState"; import { FetchRoomFn } from "./ListNotificationState"; +import { DefaultTagID } from "../room-list/models"; +import RoomListStore from "../room-list/RoomListStore"; export class SpaceNotificationState extends NotificationState { public rooms: Room[] = []; // exposed only for tests @@ -74,7 +76,15 @@ export class SpaceNotificationState extends NotificationState { this._count = 0; this._color = NotificationColor.None; - for (const state of Object.values(this.states)) { + for (const [roomId, state] of Object.entries(this.states)) { + const roomTags = RoomListStore.instance.getTagsForRoom(this.rooms.find(r => r.roomId === roomId)); + + // We ignore unreads in LowPriority rooms, see https://github.com/vector-im/element-web/issues/16836 + if ( + roomTags.includes(DefaultTagID.LowPriority) && + state.color === NotificationColor.Bold + ) continue; + this._count += state.count; this._color = Math.max(this.color, state.color); } diff --git a/src/stores/room-list/Interface.ts b/src/stores/room-list/Interface.ts new file mode 100644 index 00000000000..ab538709896 --- /dev/null +++ b/src/stores/room-list/Interface.ts @@ -0,0 +1,107 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { Room } from "matrix-js-sdk/src/models/room"; +import type { EventEmitter } from "events"; +import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { RoomUpdateCause, TagID } from "./models"; +import { IFilterCondition } from "./filters/IFilterCondition"; + +export enum RoomListStoreEvent { + // The event/channel which is called when the room lists have been changed. + ListsUpdate = "lists_update", +} + +export interface RoomListStore extends EventEmitter { + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + get orderedLists(): ITagMap; + + /** + * Set the sort algorithm for the specified tag. + * @param tagId the tag to set the algorithm for + * @param sort the sort algorithm to set to + */ + setTagSorting(tagId: TagID, sort: SortAlgorithm): void; + + /** + * Get the sort algorithm for the specified tag. + * @param tagId tag to get the sort algorithm for + * @returns the sort algorithm + */ + getTagSorting(tagId: TagID): SortAlgorithm; + + /** + * Set the list algorithm for the specified tag. + * @param tagId the tag to set the algorithm for + * @param order the list algorithm to set to + */ + setListOrder(tagId: TagID, order: ListAlgorithm): void; + + /** + * Get the list algorithm for the specified tag. + * @param tagId tag to get the list algorithm for + * @returns the list algorithm + */ + getListOrder(tagId: TagID): ListAlgorithm; + + /** + * 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 params.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. + */ + regenerateAllLists(params: { trigger: boolean }): void; + + /** + * 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. + */ + addFilter(filter: IFilterCondition): Promise; + + /** + * 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. + */ + removeFilter(filter: IFilterCondition): void; + + /** + * 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. + */ + getTagsForRoom(room: Room): TagID[]; + + /** + * 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. + */ + manualRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; +} diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index e654d7a527d..a44252a7d7c 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -37,18 +37,15 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators"; +import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; interface IState { // state is tracked in underlying classes } -/** - * The event/channel which is called when the room lists have been changed. Raised - * with one argument: the instance of the store. - */ -export const LISTS_UPDATE_EVENT = "lists_update"; +export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate; -export class RoomListStoreClass extends AsyncStoreWithClient { +export class RoomListStoreClass extends AsyncStoreWithClient implements Interface { /** * Set to true if you're running tests on the store. Should not be touched in * any other environment. @@ -365,7 +362,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.updatesInhibited = false; } - public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { + public setTagSorting(tagId: TagID, sort: SortAlgorithm) { this.setAndPersistTagSorting(tagId, sort); this.updateFn.trigger(); } @@ -622,9 +619,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } export default class RoomListStore { - private static internalInstance: RoomListStoreClass; + private static internalInstance: Interface; - public static get instance(): RoomListStoreClass { + public static get instance(): Interface { if (!RoomListStore.internalInstance) { RoomListStore.internalInstance = new RoomListStoreClass(); } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 4a685532bf2..9aa13cd3267 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -33,6 +33,7 @@ import { WidgetKind, } from "matrix-widget-api"; import { EventEmitter } from "events"; +import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent } from "matrix-js-sdk/src/client"; @@ -134,6 +135,7 @@ export class ElementWidget extends Widget { } export class StopGapWidget extends EventEmitter { + private client: MatrixClient; private messaging: ClientWidgetApi; private mockWidget: ElementWidget; private scalarToken: string; @@ -143,12 +145,13 @@ export class StopGapWidget extends EventEmitter { constructor(private appTileProps: IAppTileProps) { super(); - let app = appTileProps.app; + this.client = MatrixClientPeg.get(); + let app = appTileProps.app; // Backwards compatibility: not all old widgets have a creatorUserId if (!app.creatorUserId) { app = objectShallowClone(app); // clone to prevent accidental mutation - app.creatorUserId = MatrixClientPeg.get().getUserId(); + app.creatorUserId = this.client.getUserId(); } this.mockWidget = new ElementWidget(app); @@ -189,7 +192,7 @@ export class StopGapWidget extends EventEmitter { const fromCustomisation = WidgetVariableCustomisations?.provideVariables?.() ?? {}; const defaults: ITemplateParams = { widgetRoomId: this.roomId, - currentUserId: MatrixClientPeg.get().getUserId(), + currentUserId: this.client.getUserId(), userDisplayName: OwnProfileStore.instance.displayName, userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), clientId: ELEMENT_CLIENT_ID, @@ -246,8 +249,10 @@ export class StopGapWidget extends EventEmitter { */ public startMessaging(iframe: HTMLIFrameElement): any { if (this.started) return; + const allowedCapabilities = this.appTileProps.whitelistCapabilities || []; const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId); + this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("ready", () => this.emit("ready")); @@ -288,7 +293,7 @@ export class StopGapWidget extends EventEmitter { // Populate the map of "read up to" events for this widget with the current event in every room. // This is a bit inefficient, but should be okay. We do this for all rooms in case the widget // requests timeline capabilities in other rooms down the road. It's just easier to manage here. - for (const room of MatrixClientPeg.get().getRooms()) { + for (const room of this.client.getRooms()) { // Timelines are most recent last const events = room.getLiveTimeline()?.getEvents() || []; const roomEvent = events[events.length - 1]; @@ -297,8 +302,9 @@ export class StopGapWidget extends EventEmitter { } // Attach listeners for feeding events - the underlying widget classes handle permissions for us - MatrixClientPeg.get().on(ClientEvent.Event, this.onEvent); - MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(ClientEvent.Event, this.onEvent); + this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.messaging.on(`action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`, (ev: CustomEvent) => { @@ -349,7 +355,7 @@ export class StopGapWidget extends EventEmitter { // noinspection JSIgnoredPromiseFromCall IntegrationManagers.sharedInstance().getPrimaryManager().open( - MatrixClientPeg.get().getRoom(RoomViewStore.instance.getRoomId()), + this.client.getRoom(RoomViewStore.instance.getRoomId()), `type_${integType}`, integId, ); @@ -414,14 +420,13 @@ export class StopGapWidget extends EventEmitter { WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId); this.messaging = null; - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().off(ClientEvent.Event, this.onEvent); - MatrixClientPeg.get().off(MatrixEventEvent.Decrypted, this.onEventDecrypted); - } + this.client.off(ClientEvent.Event, this.onEvent); + this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } private onEvent = (ev: MatrixEvent) => { - MatrixClientPeg.get().decryptEventIfNeeded(ev); + this.client.decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; this.feedEvent(ev); }; @@ -431,6 +436,12 @@ export class StopGapWidget extends EventEmitter { this.feedEvent(ev); }; + private onToDeviceEvent = async (ev: MatrixEvent) => { + await this.client.decryptEventIfNeeded(ev); + if (ev.isDecryptionFailure()) return; + await this.messaging.feedToDevice(ev.getEffectiveEvent(), ev.isEncrypted()); + }; + private feedEvent(ev: MatrixEvent) { if (!this.messaging) return; @@ -451,7 +462,7 @@ export class StopGapWidget extends EventEmitter { // Timelines are most recent last, so reverse the order and limit ourselves to 100 events // to avoid overusing the CPU. - const timeline = MatrixClientPeg.get().getRoom(ev.getRoomId()).getLiveTimeline(); + const timeline = this.client.getRoom(ev.getRoomId()).getLiveTimeline(); const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); for (const timelineEvent of events) { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 3b617e6f314..8fe18dbc8c0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import { IOpenIDCredentials, IOpenIDUpdate, ISendEventDetails, + ITurnServer, + IRoomEvent, MatrixCapabilities, OpenIDRequestState, SimpleObservable, @@ -29,6 +31,7 @@ import { WidgetEventCapability, WidgetKind, } from "matrix-widget-api"; +import { ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -61,6 +64,12 @@ function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps)); } +const normalizeTurnServer = ({ urls, username, credential }: IClientTurnServer): ITurnServer => ({ + uris: urls, + username, + password: credential, +}); + export class StopGapWidgetDriver extends WidgetDriver { private allowedCapabilities: Set; @@ -182,6 +191,49 @@ export class StopGapWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } + public async sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + const client = MatrixClientPeg.get(); + + if (encrypted) { + const deviceInfoMap = await client.crypto.deviceList.downloadKeys(Object.keys(contentMap), false); + + await Promise.all( + Object.entries(contentMap).flatMap(([userId, userContentMap]) => + Object.entries(userContentMap).map(async ([deviceId, content]) => { + if (deviceId === "*") { + // Send the message to all devices we have keys for + await client.encryptAndSendToDevices( + Object.values(deviceInfoMap[userId]).map(deviceInfo => ({ + userId, deviceInfo, + })), + content, + ); + } else { + // Send the message to a specific device + await client.encryptAndSendToDevices( + [{ userId, deviceInfo: deviceInfoMap[userId][deviceId] }], + content, + ); + } + }), + ), + ); + } else { + await client.queueToDevice({ + eventType, + batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) => + Object.entries(userContentMap).map(([deviceId, content]) => + ({ userId, deviceId, payload: content }), + ), + ), + }); + } + } + private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] { const client = MatrixClientPeg.get(); if (!client) throw new Error("Not attached to a client"); @@ -197,7 +249,7 @@ export class StopGapWidgetDriver extends WidgetDriver { msgtype: string | undefined, limitPerRoom: number, roomIds: (string | Symbols.AnyRoom)[] = null, - ): Promise { + ): Promise { limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const rooms = this.pickRooms(roomIds); @@ -224,7 +276,7 @@ export class StopGapWidgetDriver extends WidgetDriver { stateKey: string | undefined, limitPerRoom: number, roomIds: (string | Symbols.AnyRoom)[] = null, - ): Promise { + ): Promise { limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const rooms = this.pickRooms(roomIds); @@ -282,4 +334,36 @@ export class StopGapWidgetDriver extends WidgetDriver { public async navigate(uri: string): Promise { navigateToPermalink(uri); } + + public async* getTurnServers(): AsyncGenerator { + const client = MatrixClientPeg.get(); + if (!client.pollingTurnServers || !client.getTurnServers().length) return; + + let setTurnServer: (server: ITurnServer) => void; + let setError: (error: Error) => void; + + const onTurnServers = ([server]: IClientTurnServer[]) => setTurnServer(normalizeTurnServer(server)); + const onTurnServersError = (error: Error, fatal: boolean) => { if (fatal) setError(error); }; + + client.on(ClientEvent.TurnServers, onTurnServers); + client.on(ClientEvent.TurnServersError, onTurnServersError); + + try { + const initialTurnServer = client.getTurnServers()[0]; + yield normalizeTurnServer(initialTurnServer); + + // Repeatedly listen for new TURN servers until an error occurs or + // the caller stops this generator + while (true) { + yield await new Promise((resolve, reject) => { + setTurnServer = resolve; + setError = reject; + }); + } + } finally { + // The loop was broken - clean up + client.off(ClientEvent.TurnServers, onTurnServers); + client.off(ClientEvent.TurnServersError, onTurnServersError); + } + } } diff --git a/src/utils/GroupCallUtils.ts b/src/utils/GroupCallUtils.ts new file mode 100644 index 00000000000..3af6a2b07a0 --- /dev/null +++ b/src/utils/GroupCallUtils.ts @@ -0,0 +1,175 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventTimeline, MatrixClient, MatrixEvent, RoomState } from "matrix-js-sdk/src/matrix"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import { deepCopy } from "matrix-js-sdk/src/utils"; + +export const STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour + +export const CALL_STATE_EVENT_TYPE = new UnstableValue("m.call", "org.matrix.msc3401.call"); +export const CALL_MEMBER_STATE_EVENT_TYPE = new UnstableValue("m.call.member", "org.matrix.msc3401.call.member"); +const CALL_STATE_EVENT_TERMINATED = "m.terminated"; + +interface MDevice { + ["m.device_id"]: string; +} + +interface MCall { + ["m.call_id"]: string; + ["m.devices"]: Array; +} + +interface MCallMemberContent { + ["m.expires_ts"]: number; + ["m.calls"]: Array; +} + +const getRoomState = (client: MatrixClient, roomId: string): RoomState => { + return client.getRoom(roomId) + ?.getLiveTimeline() + ?.getState?.(EventTimeline.FORWARDS); +}; + +/** + * Returns all room state events for the stable and unstable type value. + */ +const getRoomStateEvents = ( + client: MatrixClient, + roomId: string, + type: UnstableValue, +): MatrixEvent[] => { + const roomState = getRoomState(client, roomId); + if (!roomState) return []; + + return [ + ...roomState.getStateEvents(type.name), + ...roomState.getStateEvents(type.altName), + ]; +}; + +/** + * Finds the latest, non-terminated call state event. + */ +export const getGroupCall = (client: MatrixClient, roomId: string): MatrixEvent => { + return getRoomStateEvents(client, roomId, CALL_STATE_EVENT_TYPE) + .sort((a: MatrixEvent, b: MatrixEvent) => b.getTs() - a.getTs()) + .find((event: MatrixEvent) => { + return !(CALL_STATE_EVENT_TERMINATED in event.getContent()); + }); +}; + +/** + * Finds the "m.call.member" events for an "m.call" event. + * + * @returns {MatrixEvent[]} non-expired "m.call.member" events for the call + */ +export const useConnectedMembers = (client: MatrixClient, callEvent: MatrixEvent): MatrixEvent[] => { + if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return []; + + const callId = callEvent.getStateKey(); + const now = Date.now(); + + return getRoomStateEvents(client, callEvent.getRoomId(), CALL_MEMBER_STATE_EVENT_TYPE) + .filter((callMemberEvent: MatrixEvent): boolean => { + const { + ["m.expires_ts"]: expiresTs, + ["m.calls"]: calls, + } = callMemberEvent.getContent(); + + // state event expired + if (expiresTs && expiresTs < now) return false; + + return !!calls?.find((call: MCall) => call["m.call_id"] === callId); + }) || []; +}; + +/** + * Removes a list of devices from a call. + * Only works for the current user's devices. + */ +const removeDevices = async (client: MatrixClient, callEvent: MatrixEvent, deviceIds: string[]): Promise => { + if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return; + + const roomId = callEvent.getRoomId(); + const roomState = getRoomState(client, roomId); + if (!roomState) return; + + const callMemberEvent = roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.name, client.getUserId()) + ?? roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.altName, client.getUserId()); + const callMemberEventContent = callMemberEvent?.getContent(); + if ( + !Array.isArray(callMemberEventContent?.["m.calls"]) + || callMemberEventContent?.["m.calls"].length === 0 + ) { + return; + } + + // copy the content to prevent mutations + const newContent = deepCopy(callMemberEventContent); + const callId = callEvent.getStateKey(); + let changed = false; + + newContent["m.calls"].forEach((call: MCall) => { + // skip other calls + if (call["m.call_id"] !== callId) return; + + call["m.devices"] = call["m.devices"]?.filter((device: MDevice) => { + if (deviceIds.includes(device["m.device_id"])) { + changed = true; + return false; + } + + return true; + }); + }); + + if (changed) { + // only send a new state event if there has been a change + newContent["m.expires_ts"] = Date.now() + STUCK_DEVICE_TIMEOUT_MS; + await client.sendStateEvent( + roomId, + CALL_MEMBER_STATE_EVENT_TYPE.name, + newContent, + client.getUserId(), + ); + } +}; + +/** + * Removes the current device from a call. + */ +export const removeOurDevice = async (client: MatrixClient, callEvent: MatrixEvent) => { + return removeDevices(client, callEvent, [client.getDeviceId()]); +}; + +/** + * Removes all devices of the current user that have not been seen within the STUCK_DEVICE_TIMEOUT_MS. + * Does per default not remove the current device unless includeCurrentDevice is true. + * + * @param {boolean} includeCurrentDevice - Whether to include the current device of this session here. + */ +export const fixStuckDevices = async (client: MatrixClient, callEvent: MatrixEvent, includeCurrentDevice: boolean) => { + const now = Date.now(); + const { devices: myDevices } = await client.getDevices(); + const currentDeviceId = client.getDeviceId(); + const devicesToBeRemoved = myDevices.filter(({ last_seen_ts: lastSeenTs, device_id: deviceId }) => { + return lastSeenTs + && (deviceId !== currentDeviceId || includeCurrentDevice) + && (now - lastSeenTs) > STUCK_DEVICE_TIMEOUT_MS; + }).map(d => d.device_id); + return removeDevices(client, callEvent, devicesToBeRemoved); +}; diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index cd442f213b8..e4790eaad2a 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -17,6 +17,7 @@ limitations under the License. import { Capability, EventDirection, + EventKind, getTimelineRoomIDFromCapability, isTimelineCapability, isTimelineCapabilityFor, @@ -134,7 +135,7 @@ export class CapabilityText { }; private static bylineFor(eventCap: WidgetEventCapability): TranslatedString { - if (eventCap.isState) { + if (eventCap.kind === EventKind.State) { return !eventCap.keyStr ? _t("with an empty state key") : _t("with state key %(stateKey)s", { stateKey: eventCap.keyStr }); @@ -143,6 +144,8 @@ export class CapabilityText { } public static for(capability: Capability, kind: WidgetKind): TranslatedCapabilityText { + // TODO: Support MSC3819 (to-device capabilities) + // First see if we have a super simple line of text to provide back if (CapabilityText.simpleCaps[capability]) { const textForKind = CapabilityText.simpleCaps[capability]; @@ -184,13 +187,13 @@ export class CapabilityText { // Special case room messages so they show up a bit cleaner to the user. Result is // effectively "Send images" instead of "Send messages... of type images" if we were // to handle the msgtype nuances in this function. - if (!eventCap.isState && eventCap.eventType === EventType.RoomMessage) { + if (eventCap.kind === EventKind.Event && eventCap.eventType === EventType.RoomMessage) { return CapabilityText.forRoomMessageCap(eventCap, kind); } // See if we have a static line of text to provide for the given event type and // direction. The hope is that we do for common event types for friendlier copy. - const evSendRecv = eventCap.isState + const evSendRecv = eventCap.kind === EventKind.State ? CapabilityText.stateSendRecvCaps : CapabilityText.nonStateSendRecvCaps; if (evSendRecv[eventCap.eventType]) { diff --git a/test/accessibility/RovingTabIndex-test.tsx b/test/accessibility/RovingTabIndex-test.tsx index 708fc3c928a..9f7364658d8 100644 --- a/test/accessibility/RovingTabIndex-test.tsx +++ b/test/accessibility/RovingTabIndex-test.tsx @@ -15,8 +15,7 @@ limitations under the License. */ import * as React from "react"; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from "enzyme"; +import { render } from "@testing-library/react"; import { IState, @@ -32,10 +31,10 @@ const Button = (props) => { return