Merge branch 'actions/upgrade-deps' of github.com:matrix-org/matrix-react-sdk into actions/upgrade-deps
Conflicts: yarn.lock
32
CHANGELOG.md
|
@ -1,3 +1,35 @@
|
|||
Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.52.0) (2022-08-16)
|
||||
=====================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Device manager - New device tile info design ([\#9122](https://github.com/matrix-org/matrix-react-sdk/pull/9122)). Contributed by @kerryarchibald.
|
||||
* Device manager generic settings subsection component ([\#9147](https://github.com/matrix-org/matrix-react-sdk/pull/9147)). Contributed by @kerryarchibald.
|
||||
* Migrate the hidden read receipts flag to new "send read receipts" option ([\#9141](https://github.com/matrix-org/matrix-react-sdk/pull/9141)).
|
||||
* Live location sharing - share location at most every 5 seconds ([\#9148](https://github.com/matrix-org/matrix-react-sdk/pull/9148)). Contributed by @kerryarchibald.
|
||||
* Increase max length of voice messages to 15m ([\#9133](https://github.com/matrix-org/matrix-react-sdk/pull/9133)). Fixes vector-im/element-web#18620.
|
||||
* Move pin drop out of labs ([\#9135](https://github.com/matrix-org/matrix-react-sdk/pull/9135)).
|
||||
* Start DM on first message ([\#8612](https://github.com/matrix-org/matrix-react-sdk/pull/8612)). Fixes vector-im/element-web#14736.
|
||||
* Remove "Add Space" button from RoomListHeader when user cannot create spaces ([\#9129](https://github.com/matrix-org/matrix-react-sdk/pull/9129)).
|
||||
* The Welcome Home Screen: Dedicated Download Apps Dialog ([\#9120](https://github.com/matrix-org/matrix-react-sdk/pull/9120)). Fixes vector-im/element-web#22921. Contributed by @justjanne.
|
||||
* The Welcome Home Screen: "Submit Feedback" pane ([\#9090](https://github.com/matrix-org/matrix-react-sdk/pull/9090)). Fixes vector-im/element-web#22918. Contributed by @justjanne.
|
||||
* New User Onboarding Task List ([\#9083](https://github.com/matrix-org/matrix-react-sdk/pull/9083)). Fixes vector-im/element-web#22919. Contributed by @justjanne.
|
||||
* Add support for disabling spell checking ([\#8604](https://github.com/matrix-org/matrix-react-sdk/pull/8604)). Fixes vector-im/element-web#21901.
|
||||
* Live location share - leave maximised map open when beacons expire ([\#9098](https://github.com/matrix-org/matrix-react-sdk/pull/9098)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Some slash-commands (`/myroomnick`) have temporarily been disabled before the first message in a DM is sent. ([\#9193](https://github.com/matrix-org/matrix-react-sdk/pull/9193)).
|
||||
* Use stable reference for active tab in tabbedView ([\#9145](https://github.com/matrix-org/matrix-react-sdk/pull/9145)). Contributed by @kerryarchibald.
|
||||
* Fix pillification sometimes doubling up ([\#9152](https://github.com/matrix-org/matrix-react-sdk/pull/9152)). Fixes vector-im/element-web#23036.
|
||||
* Fix composer padding ([\#9137](https://github.com/matrix-org/matrix-react-sdk/pull/9137)). Fixes vector-im/element-web#22992.
|
||||
* Fix highlights not being applied to plaintext messages ([\#9126](https://github.com/matrix-org/matrix-react-sdk/pull/9126)). Fixes vector-im/element-web#22787.
|
||||
* Fix dismissing edit composer when change was undone ([\#9109](https://github.com/matrix-org/matrix-react-sdk/pull/9109)). Fixes vector-im/element-web#22932.
|
||||
* 1-to-1 DM rooms with bots now act like DM rooms instead of multi-user-rooms before ([\#9124](https://github.com/matrix-org/matrix-react-sdk/pull/9124)). Fixes vector-im/element-web#22894.
|
||||
* Apply inline start padding to selected lines on modern layout only ([\#9006](https://github.com/matrix-org/matrix-react-sdk/pull/9006)). Fixes vector-im/element-web#22768. Contributed by @luixxiul.
|
||||
* Peek into world-readable rooms from spotlight ([\#9115](https://github.com/matrix-org/matrix-react-sdk/pull/9115)). Fixes vector-im/element-web#22862.
|
||||
* Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935.
|
||||
* Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824.
|
||||
* The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)).
|
||||
|
||||
Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02)
|
||||
=====================================================================================================
|
||||
|
||||
|
|
|
@ -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] });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
201
cypress/e2e/widgets/widget-pip-close.spec.ts
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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 = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Demo Widget</title>
|
||||
<script>
|
||||
window.onmessage = ev => {
|
||||
if (ev.data.action === 'capabilities') {
|
||||
window.parent.postMessage(Object.assign({
|
||||
response: {
|
||||
capabilities: []
|
||||
},
|
||||
}, ev.data), '*');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="demo">Demo</button>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// mostly copied from src/utils/WidgetUtils.waitForRoomWidget with small modifications
|
||||
function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: string, add: boolean): Promise<void> {
|
||||
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<string>("@powerLevelsChanged"),
|
||||
cy.get<string>("@botJoined"),
|
||||
cy.get<string>("@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');
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<void>;
|
||||
setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Chainable<void>;
|
||||
|
||||
/**
|
||||
* 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<T>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T>;
|
||||
getSettingValue<T>(settingName: string, roomId?: string, excludeDefault?: boolean): Chainable<T>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -94,7 +94,7 @@
|
|||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.18",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -78,10 +78,6 @@ $activeBorderColor: $primary-content;
|
|||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
> .mx_SpaceItem {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceButton_toggleCollapse {
|
||||
|
@ -290,6 +286,11 @@ $activeBorderColor: $primary-content;
|
|||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceTreeLevel {
|
||||
// Indent subspaces
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceButton_avatarWrapper {
|
||||
|
@ -378,11 +379,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 {
|
||||
|
|
|
@ -177,6 +177,10 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
.mx_LabelledCheckbox {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
.mx_Checkbox {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: $spacing-8;
|
||||
flex: 1;
|
||||
|
||||
.mx_LinkPreviewWidget_image,
|
||||
.mx_LinkPreviewWidget_caption {
|
||||
|
|
84
res/css/views/user-onboarding/_UserOnboardingButton.pcss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
3
res/img/e2e/verified-deprecated.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#010101"/>
|
||||
</svg>
|
After Width: | Height: | Size: 658 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#010101"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 663 B |
3
res/img/e2e/warning-deprecated.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="#020202"/>
|
||||
</svg>
|
After Width: | Height: | Size: 673 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="#020202"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 678 B |
3
res/img/element-icons/settings/inactive.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.33333 0.333008C0.6 0.333008 0 0.933008 0 1.66634L0.00666682 3.78634C0.00666682 4.13967 0.146667 4.47301 0.393333 4.72634L2.66667 6.99967L0.393333 9.28634C0.146667 9.53301 0.00666682 9.87301 0.00666682 10.2263L0 12.333C0 13.0663 0.6 13.6663 1.33333 13.6663H6.66667C7.4 13.6663 8 13.0663 8 12.333V10.2263C8 9.87301 7.86 9.53301 7.61333 9.28634L5.33333 6.99967L7.60667 4.73301C7.86 4.47967 8 4.13967 8 3.78634V1.66634C8 0.933008 7.4 0.333008 6.66667 0.333008H1.33333ZM6.66667 10.273V11.6663C6.66667 12.033 6.36667 12.333 6 12.333H2C1.63333 12.333 1.33333 12.033 1.33333 11.6663V10.273C1.33333 10.093 1.40667 9.92634 1.52667 9.79967L4 7.33301L6.47333 9.80634C6.59333 9.92634 6.66667 10.0997 6.66667 10.273Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 839 B |
|
@ -1,5 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#4e5054" stroke-linecap="round" stroke-width="1.3" transform="translate(1 1)">
|
||||
<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="1.3" transform="translate(1 1)">
|
||||
<path d="m.5.5 3.85868526 3.25422271"/>
|
||||
<path d="m8.13193273.56042139-3.77324747 3.19380132"/>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 340 B |
|
@ -217,6 +217,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: #454545;
|
||||
|
|
|
@ -201,6 +201,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 */
|
||||
|
|
4
src/@types/global.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -632,13 +632,13 @@ export function topicToHtml(
|
|||
emojiBodyElements = formatEmojis(topic, false);
|
||||
}
|
||||
|
||||
return isFormattedTopic ?
|
||||
<span
|
||||
key="body"
|
||||
return isFormattedTopic
|
||||
? <span
|
||||
ref={ref}
|
||||
dangerouslySetInnerHTML={{ __html: safeTopic }}
|
||||
dir="auto"
|
||||
/> : <span key="body" ref={ref} dir="auto">
|
||||
/>
|
||||
: <span ref={ref} dir="auto">
|
||||
{ emojiBodyElements || topic }
|
||||
</span>;
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
|||
import VoipUserMapper from './VoipUserMapper';
|
||||
import { htmlSerializeFromMdIfNeeded } from './editor/serialize';
|
||||
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
||||
import { isLocalRoom } from './utils/localRoom/isLocalRoom';
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -206,6 +207,12 @@ function successSync(value: any) {
|
|||
return success(Promise.resolve(value));
|
||||
}
|
||||
|
||||
const isCurrentLocalRoom = (): boolean => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
|
||||
return isLocalRoom(room);
|
||||
};
|
||||
|
||||
/* Disable the "unexpected this" error for these commands - all of the run
|
||||
* functions are called with `this` bound to the Command instance.
|
||||
*/
|
||||
|
@ -297,6 +304,7 @@ export const Commands = [
|
|||
command: 'upgraderoom',
|
||||
args: '<new_version>',
|
||||
description: _td('Upgrades a room to a new version'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -380,6 +388,7 @@ export const Commands = [
|
|||
aliases: ['roomnick'],
|
||||
args: '<display_name>',
|
||||
description: _td('Changes your display nickname in the current room only'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -399,6 +408,7 @@ export const Commands = [
|
|||
command: 'roomavatar',
|
||||
args: '[<mxc_url>]',
|
||||
description: _td('Changes the avatar of the current room'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
let promise = Promise.resolve(args);
|
||||
if (!args) {
|
||||
|
@ -417,6 +427,7 @@ export const Commands = [
|
|||
command: 'myroomavatar',
|
||||
args: '[<mxc_url>]',
|
||||
description: _td('Changes your avatar in this current room only'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
|
@ -462,6 +473,7 @@ export const Commands = [
|
|||
command: 'topic',
|
||||
args: '[<topic>]',
|
||||
description: _td('Gets or sets the room topic'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (args) {
|
||||
|
@ -498,6 +510,7 @@ export const Commands = [
|
|||
command: 'roomname',
|
||||
args: '<name>',
|
||||
description: _td('Sets the room name'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
return success(MatrixClientPeg.get().setRoomName(roomId, args));
|
||||
|
@ -512,7 +525,7 @@ export const Commands = [
|
|||
args: '<user-id> [<reason>]',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
analyticsName: "Invite",
|
||||
isEnabled: () => shouldShowComponent(UIComponent.InviteUsers),
|
||||
isEnabled: () => !isCurrentLocalRoom() && shouldShowComponent(UIComponent.InviteUsers),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const [address, reason] = args.split(/\s+(.+)/);
|
||||
|
@ -694,6 +707,7 @@ export const Commands = [
|
|||
args: '[<room-address>]',
|
||||
description: _td('Leave room'),
|
||||
analyticsName: "Part",
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -746,6 +760,7 @@ export const Commands = [
|
|||
aliases: ["kick"],
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Removes user with given id from this room'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
|
@ -762,6 +777,7 @@ export const Commands = [
|
|||
command: 'ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Bans user with given id'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
|
@ -778,6 +794,7 @@ export const Commands = [
|
|||
command: 'unban',
|
||||
args: '<user-id>',
|
||||
description: _td('Unbans user with given ID'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
|
@ -857,7 +874,8 @@ export const Commands = [
|
|||
isEnabled(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId())
|
||||
&& !isLocalRoom(room);
|
||||
},
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
|
@ -897,7 +915,8 @@ export const Commands = [
|
|||
isEnabled(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId())
|
||||
&& !isLocalRoom(room);
|
||||
},
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
|
@ -936,7 +955,9 @@ export const Commands = [
|
|||
command: 'addwidget',
|
||||
args: '<url | embed code | Jitsi url>',
|
||||
description: _td('Adds a custom widget by URL to the room'),
|
||||
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets) && shouldShowComponent(UIComponent.AddIntegrations),
|
||||
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& shouldShowComponent(UIComponent.AddIntegrations)
|
||||
&& !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, widgetUrl) {
|
||||
if (!widgetUrl) {
|
||||
return reject(newTranslatableError("Please supply a widget URL or embed code"));
|
||||
|
@ -1059,6 +1080,7 @@ export const Commands = [
|
|||
new Command({
|
||||
command: 'discardsession',
|
||||
description: _td('Forces the current outbound group session in an encrypted room to be discarded'),
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId) {
|
||||
try {
|
||||
MatrixClientPeg.get().forceDiscardSession(roomId);
|
||||
|
@ -1074,7 +1096,7 @@ export const Commands = [
|
|||
command: 'remakeolm',
|
||||
description: _td('Developer command: Discards the current outbound group session and sets up new Olm sessions'),
|
||||
isEnabled: () => {
|
||||
return SettingsStore.getValue("developerMode");
|
||||
return SettingsStore.getValue("developerMode") && !isCurrentLocalRoom();
|
||||
},
|
||||
runFn: (roomId) => {
|
||||
try {
|
||||
|
@ -1125,6 +1147,7 @@ export const Commands = [
|
|||
command: "whois",
|
||||
description: _td("Displays information about a user"),
|
||||
args: "<user-id>",
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, userId) {
|
||||
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
||||
return reject(this.getUsage());
|
||||
|
@ -1160,7 +1183,7 @@ export const Commands = [
|
|||
description: _td("Switches to this room's virtual room, if it has one"),
|
||||
category: CommandCategories.advanced,
|
||||
isEnabled(): boolean {
|
||||
return CallHandler.instance.getSupportsVirtualRooms();
|
||||
return CallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom();
|
||||
},
|
||||
runFn: (roomId) => {
|
||||
return success((async () => {
|
||||
|
@ -1244,6 +1267,7 @@ export const Commands = [
|
|||
command: "holdcall",
|
||||
description: _td("Places the call in the current room on hold"),
|
||||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
|
@ -1258,6 +1282,7 @@ export const Commands = [
|
|||
command: "unholdcall",
|
||||
description: _td("Takes the call in the current room off hold"),
|
||||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
|
@ -1272,6 +1297,7 @@ export const Commands = [
|
|||
command: "converttodm",
|
||||
description: _td("Converts the room to a DM"),
|
||||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return success(guessAndSetDMRoom(room, true));
|
||||
|
@ -1282,6 +1308,7 @@ export const Commands = [
|
|||
command: "converttoroom",
|
||||
description: _td("Converts the DM to a room"),
|
||||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return success(guessAndSetDMRoom(room, false));
|
||||
|
|
|
@ -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 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 = [];
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
||||
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
|
||||
JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">;
|
||||
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], 'ref'>>;
|
||||
|
||||
export type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
|
||||
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<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
|
||||
static defaultProps = {
|
||||
element: 'div' as keyof ReactHTML,
|
||||
};
|
||||
|
||||
export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
|||
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<IProps> {
|
|||
|
||||
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 (<div
|
||||
{...otherProps}
|
||||
ref={this.containerRef}
|
||||
style={style}
|
||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||
onWheel={onWheel}
|
||||
return React.createElement(element, {
|
||||
...otherProps,
|
||||
ref: this.containerRef,
|
||||
className: classNames("mx_AutoHideScrollbar", className),
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order by default.
|
||||
tabIndex={tabIndex ?? -1}
|
||||
>
|
||||
{ children }
|
||||
</div>);
|
||||
tabIndex: tabIndex ?? -1,
|
||||
}, children);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,35 +225,57 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
|
||||
protected renderMenu(hasBackground = this.props.hasBackground) {
|
||||
const position: Partial<Writeable<DOMRect>> = {};
|
||||
const props = this.props;
|
||||
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 (props.top) {
|
||||
position.top = props.top;
|
||||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
'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<IProps, IState> {
|
|||
|
||||
let body = <>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
{ children }
|
||||
</>;
|
||||
|
||||
if (props.focusLock) {
|
||||
if (focusLock) {
|
||||
body = <FocusLock>
|
||||
{ body }
|
||||
</FocusLock>;
|
||||
|
@ -379,7 +401,7 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.onKeyDown}>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<div
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
className={classNames("mx_ContextualMenu_wrapper", wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onClick={this.onClick}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
|
@ -390,7 +412,8 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
role={managed ? "menu" : undefined}
|
||||
{...props}
|
||||
>
|
||||
{ body }
|
||||
</div>
|
||||
|
|
|
@ -85,7 +85,7 @@ const UserWelcomeTop = () => {
|
|||
</MiniAvatarUploader>
|
||||
|
||||
<h1>{ _tDom("Welcome %(name)s", { name: ownProfile.displayName }) }</h1>
|
||||
<h4>{ _tDom("Now, let's help you get started") }</h4>
|
||||
<h2>{ _tDom("Now, let's help you get started") }</h2>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -97,8 +97,8 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
|
||||
}
|
||||
|
||||
let introSection;
|
||||
if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) {
|
||||
let introSection: JSX.Element;
|
||||
if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) {
|
||||
introSection = <UserWelcomeTop />;
|
||||
} else {
|
||||
const brandingConfig = SdkConfig.getObject("branding");
|
||||
|
@ -107,11 +107,11 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
introSection = <React.Fragment>
|
||||
<img src={logoUrl} alt={config.brand} />
|
||||
<h1>{ _tDom("Welcome to %(appName)s", { appName: config.brand }) }</h1>
|
||||
<h4>{ _tDom("Own your conversations.") }</h4>
|
||||
<h2>{ _tDom("Own your conversations.") }</h2>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default" element="main">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
{ introSection }
|
||||
<div className="mx_HomePage_default_buttons">
|
||||
|
|
|
@ -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<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
|
||||
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "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<ComponentProps<typeof AutoHideScrollbar>, "onWheel
|
|||
verticalScrollsHorizontally?: boolean;
|
||||
|
||||
children: React.ReactNode;
|
||||
className: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface IState {
|
||||
leftIndicatorOffset: string;
|
||||
rightIndicatorOffset: string;
|
||||
}
|
||||
|
||||
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
||||
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
||||
export default class IndicatorScrollbar<
|
||||
T extends keyof JSX.IntrinsicElements,
|
||||
> extends React.Component<IProps<T>, IState> {
|
||||
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
|
||||
private scrollElement: HTMLDivElement;
|
||||
private likelyTrackpadUser: boolean = null;
|
||||
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||
|
||||
constructor(props: IProps) {
|
||||
constructor(props: IProps<T>) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -65,7 +66,7 @@ export default class IndicatorScrollbar extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
public componentDidUpdate(prevProps: IProps<T>): void {
|
||||
const prevLen = React.Children.count(prevProps.children);
|
||||
const curLen = React.Children.count(this.props.children);
|
||||
// check overflow only if amount of children changes.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -390,6 +393,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onVisibilityChange={this.refreshStickyHeaders}
|
||||
/>
|
||||
) }
|
||||
<UserOnboardingButton
|
||||
selected={this.props.pageType === PageType.HomePage}
|
||||
minimized={this.props.isMinimized}
|
||||
/>
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
|
|
|
@ -689,6 +689,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
data-collapsed={this.props.collapseLhs ? true : undefined}
|
||||
>
|
||||
<LeftPanel
|
||||
pageType={this.props.page_type as PageTypes}
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
|
|
|
@ -504,7 +504,7 @@ export const useRoomHierarchy = (space: Room): {
|
|||
loadMore(pageSize?: number): Promise<void>;
|
||||
} => {
|
||||
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||
const [roomHierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
const cli = useContext(MatrixClientContext);
|
||||
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
|
||||
|
||||
const scrollRef = useRef<AutoHideScrollbar>();
|
||||
const scrollRef = useRef<AutoHideScrollbar<"div">>();
|
||||
const [scrollState, setScrollState] = useState<IScrollState>({
|
||||
// these are estimates which update as soon as it mounts
|
||||
scrollTop: 0,
|
||||
|
|
|
@ -58,10 +58,10 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
|
|||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished("legacy")}>
|
||||
{ _t("Manually Verify by Text") }
|
||||
{ _t("Manually verify by text") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished("sas")}>
|
||||
{ _t("Interactively verify by Emoji") }
|
||||
{ _t("Interactively verify by emoji") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
|
||||
{ _t("Done") }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -376,7 +376,9 @@ const SpotlightDialog: React.FC<IProps> = ({ 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));
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -192,7 +192,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -52,8 +52,10 @@ export interface ITooltipProps {
|
|||
maxParentWidth?: number;
|
||||
}
|
||||
|
||||
export default class Tooltip extends React.Component<ITooltipProps> {
|
||||
private tooltipContainer: HTMLElement;
|
||||
type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
|
||||
|
||||
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||
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<ITooltipProps> {
|
|||
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<ITooltipProps> {
|
|||
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<ITooltipProps> {
|
|||
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 = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
|
@ -179,14 +187,10 @@ export default class Tooltip extends React.Component<ITooltipProps> {
|
|||
</div>
|
||||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
ReactDOM.render<Element>(tooltip, this.tooltipContainer);
|
||||
};
|
||||
|
||||
public render() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} />
|
||||
<div className={this.props.className}>
|
||||
{ ReactDOM.createPortal(tooltip, Tooltip.container) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
|
||||
private readonly categories: ICategory[];
|
||||
|
||||
private scrollRef = React.createRef<AutoHideScrollbar>();
|
||||
private scrollRef = React.createRef<AutoHideScrollbar<"div">>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -240,7 +240,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
render() {
|
||||
let heightBefore = 0;
|
||||
return (
|
||||
<div className="mx_EmojiPicker">
|
||||
<div className="mx_EmojiPicker" data-testid='mx_EmojiPicker'>
|
||||
<Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
|
||||
<Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
|
||||
<AutoHideScrollbar
|
||||
|
|
|
@ -134,7 +134,6 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
|||
isEmojiDisabled={this.isEmojiDisabled}
|
||||
selectedEmojis={this.state.selectedEmojis}
|
||||
showQuickReactions={true}
|
||||
data-testid='mx_ReactionPicker'
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<IOptionsButtonProps> = ({
|
|||
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<IOptionsButtonProps> = ({
|
|||
// 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<IOptionsButtonProps> = ({
|
|||
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<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
</ContextMenu>;
|
||||
}
|
||||
|
||||
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 <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_MessageActionBar_iconButton"
|
||||
title={_t("React")}
|
||||
onClick={() => {
|
||||
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}
|
||||
>
|
||||
<ThreadIcon />
|
||||
{ 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 <RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={_t("Favourite")}
|
||||
onClick={() => toggleFavourite(eventId)}
|
||||
onClick={onClick}
|
||||
onContextMenu={onClick}
|
||||
data-testid={eventId}
|
||||
>
|
||||
<StarIcon />
|
||||
|
@ -335,7 +357,11 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
this.props.onFocusChange?.(focused);
|
||||
};
|
||||
|
||||
private onReplyClick = (ev: React.MouseEvent): void => {
|
||||
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<IMessageAction
|
|||
});
|
||||
};
|
||||
|
||||
private onEditClick = (): void => {
|
||||
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<IMessageAction
|
|||
}
|
||||
|
||||
private onResendClick = (ev: React.MouseEvent): void => {
|
||||
// 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<IMessageAction
|
|||
className="mx_MessageActionBar_iconButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
onContextMenu={this.onEditClick}
|
||||
key="edit"
|
||||
>
|
||||
<EditIcon />
|
||||
|
@ -433,6 +468,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
className="mx_MessageActionBar_iconButton"
|
||||
title={_t("Delete")}
|
||||
onClick={this.onCancelClick}
|
||||
onContextMenu={this.onCancelClick}
|
||||
key="cancel"
|
||||
>
|
||||
<TrashcanIcon />
|
||||
|
@ -453,6 +489,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
className="mx_MessageActionBar_iconButton"
|
||||
title={_t("Retry")}
|
||||
onClick={this.onResendClick}
|
||||
onContextMenu={this.onResendClick}
|
||||
key="resend"
|
||||
>
|
||||
<ResendIcon />
|
||||
|
@ -475,6 +512,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
className="mx_MessageActionBar_iconButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
onContextMenu={this.onReplyClick}
|
||||
key="reply"
|
||||
>
|
||||
<ReplyIcon />
|
||||
|
|
|
@ -85,7 +85,7 @@ const EncryptionPanel: React.FC<IProps> = (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: <div>
|
||||
{ _t("One of the following may be compromised:") }
|
||||
|
|
|
@ -854,9 +854,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
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");
|
||||
|
||||
|
|
|
@ -112,7 +112,13 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
let img;
|
||||
if (image) {
|
||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img ref={this.image} style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||
<img
|
||||
ref={this.image}
|
||||
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
|
||||
src={image}
|
||||
onClick={this.onImageClick}
|
||||
alt=""
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdat
|
|||
import MessageComposerButtons from './MessageComposerButtons';
|
||||
import { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||
|
||||
let instanceCount = 0;
|
||||
|
||||
|
@ -350,6 +351,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private get showStickersButton(): boolean {
|
||||
return this.state.showStickersButton && !isLocalRoom(this.props.room);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const controls = [
|
||||
this.props.e2eStatus ?
|
||||
|
@ -475,7 +480,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||
showLocationButton={!window.electron}
|
||||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.state.showStickersButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
/> }
|
||||
{ showSendButton && (
|
||||
|
|
|
@ -372,7 +372,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
||||
await RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
||||
RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
crossSigningInfo: crossSigningInfo,
|
||||
};
|
||||
});
|
||||
console.log(this.state);
|
||||
},
|
||||
(error) => {
|
||||
if (this.unmounted) { return; }
|
||||
|
@ -178,76 +175,38 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onDeleteClick = (): void => {
|
||||
private onDeleteClick = async (): Promise<void> => {
|
||||
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",
|
||||
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,
|
||||
});
|
||||
},
|
||||
[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,
|
||||
},
|
||||
});
|
||||
}).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<any> {
|
||||
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<IProps, IState> {
|
|||
|
||||
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<IProps, IState> {
|
|||
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 }) }
|
||||
</AccessibleButton>;
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
};
|
||||
|
||||
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<IProps, IState> {
|
|||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
|
||||
const deviceWithVerification = {
|
||||
...this.props.device,
|
||||
isVerified: this.props.verified,
|
||||
};
|
||||
|
||||
if (this.props.isOwnDevice) {
|
||||
return <div className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
return <div className={classNames("mx_DevicesPanel_device", "mx_DevicesPanel_myDevice")}>
|
||||
<div className="mx_DevicesPanel_deviceTrust">
|
||||
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
|
||||
</div>
|
||||
<DeviceTile device={this.props.device}>
|
||||
<DeviceTile device={deviceWithVerification}>
|
||||
{ buttons }
|
||||
</DeviceTile>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
<SelectableDeviceTile device={this.props.device} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<SelectableDeviceTile device={deviceWithVerification} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
{ buttons }
|
||||
</SelectableDeviceTile>
|
||||
</div>
|
||||
|
|
|
@ -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<Props> = ({
|
||||
device, isLoading,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return <SettingsSubsection
|
||||
heading={_t('Current session')}
|
||||
data-testid='current-session-section'
|
||||
>
|
||||
{ isLoading && <Spinner /> }
|
||||
{ !!device && <>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
>
|
||||
<DeviceExpandDetailsButton
|
||||
data-testid='current-session-toggle-details'
|
||||
isExpanded={isExpanded}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
</DeviceTile>
|
||||
{ isExpanded && <DeviceDetails device={device} /> }
|
||||
<br />
|
||||
<DeviceVerificationStatusCard device={device} />
|
||||
</>
|
||||
}
|
||||
</SettingsSubsection>;
|
||||
};
|
||||
|
||||
export default CurrentDeviceSection;
|
81
src/components/views/settings/devices/DeviceDetails.tsx
Normal file
|
@ -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<Props> = ({ 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 <div className='mx_DeviceDetails' data-testid={`device-detail-${device.device_id}`}>
|
||||
<section className='mx_DeviceDetails_section'>
|
||||
<Heading size='h3'>{ device.display_name ?? device.device_id }</Heading>
|
||||
<DeviceVerificationStatusCard device={device} />
|
||||
</section>
|
||||
<section className='mx_DeviceDetails_section'>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
|
||||
{ metadata.map(({ heading, values }, index) => <table
|
||||
className='mxDeviceDetails_metadataTable'
|
||||
key={index}
|
||||
>
|
||||
{ heading &&
|
||||
<thead>
|
||||
<tr><th>{ heading }</th></tr>
|
||||
</thead>
|
||||
}
|
||||
<tbody>
|
||||
|
||||
{ values.map(({ label, value }) => <tr key={label}>
|
||||
<td className='mxDeviceDetails_metadataLabel'>{ label }</td>
|
||||
<td className='mxDeviceDetails_metadataValue'>{ value }</td>
|
||||
</tr>) }
|
||||
</tbody>
|
||||
</table>,
|
||||
) }
|
||||
</section>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default DeviceDetails;
|
|
@ -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<Props> = ({ isExpanded, onClick, ...rest }) => {
|
||||
return <AccessibleButton
|
||||
{...rest}
|
||||
aria-label={_t('Toggle device details')}
|
||||
kind='icon'
|
||||
className={classNames('mx_DeviceExpandDetailsButton', {
|
||||
mx_DeviceExpandDetailsButton_expanded: isExpanded,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CaretIcon className='mx_DeviceExpandDetailsButton_icon' />
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
export default DeviceExpandDetailsButton;
|
55
src/components/views/settings/devices/DeviceSecurityCard.tsx
Normal file
|
@ -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, React.FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
[DeviceSecurityVariation.Inactive]: InactiveIcon,
|
||||
[DeviceSecurityVariation.Verified]: VerifiedIcon,
|
||||
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
|
||||
};
|
||||
|
||||
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
|
||||
const Icon = VariationIcon[variation];
|
||||
return <div className={classNames('mx_DeviceSecurityCard_icon', variation)}>
|
||||
<Icon height={16} width={16} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const DeviceSecurityCard: React.FC<Props> = ({ variation, heading, description, children }) => {
|
||||
return <div className='mx_DeviceSecurityCard'>
|
||||
<DeviceSecurityIcon variation={variation} />
|
||||
<div className='mx_DeviceSecurityCard_content'>
|
||||
<p className='mx_DeviceSecurityCard_heading'>{ heading }</p>
|
||||
<p className='mx_DeviceSecurityCard_description'>{ description }</p>
|
||||
{ children }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default DeviceSecurityCard;
|
|
@ -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 <TooltipTarget
|
||||
alignment={Alignment.Top}
|
||||
|
@ -45,7 +46,8 @@ const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => {
|
|||
</Heading>;
|
||||
};
|
||||
|
||||
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: (
|
||||
<>
|
||||
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
|
||||
{
|
||||
_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 ? <span data-testid={`device-metadata-${id}`}>{ value }</span> : null
|
||||
);
|
||||
|
||||
const DeviceTile: React.FC<DeviceTileProps> = ({ 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 <div className="mx_DeviceTile">
|
||||
return <div className="mx_DeviceTile" data-testid={`device-tile-${device.device_id}`}>
|
||||
<div className="mx_DeviceTile_info" onClick={onClick}>
|
||||
<DeviceTileName device={device} />
|
||||
<div className="mx_DeviceTile_metadata">
|
||||
|
|
|
@ -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<Props> = ({
|
||||
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 <DeviceSecurityCard
|
||||
{...securityCardProps}
|
||||
/>;
|
||||
};
|
220
src/components/views/settings/devices/FilteredDeviceList.tsx
Normal file
|
@ -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 <div className='mx_FilteredDeviceList_securityCard'>
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Verified}
|
||||
heading={_t('Verified sessions')}
|
||||
description={_t(
|
||||
`For best security, sign out from any session` +
|
||||
` that you don't recognize or use anymore.`,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
;
|
||||
case DeviceSecurityVariation.Unverified:
|
||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Unverified}
|
||||
heading={_t('Unverified sessions')}
|
||||
description={_t(
|
||||
`Verify your sessions for enhanced secure messaging or sign out`
|
||||
+ ` from those you don't recognize or use anymore.`,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
;
|
||||
case DeviceSecurityVariation.Inactive:
|
||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Inactive}
|
||||
heading={_t('Inactive sessions')}
|
||||
description={_t(
|
||||
`Consider signing out from old sessions ` +
|
||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
||||
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
;
|
||||
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<NoResultsProps> = ({ filter, clearFilter }) =>
|
||||
<div className='mx_FilteredDeviceList_noResults'>
|
||||
{ getNoResultsMessage(filter) }
|
||||
{
|
||||
/* No clear filter button when filter is falsy (ie 'All') */
|
||||
!!filter &&
|
||||
<>
|
||||
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={clearFilter}
|
||||
data-testid='devices-clear-filter-btn'
|
||||
>
|
||||
{ _t('Show all') }
|
||||
</AccessibleButton>
|
||||
</>
|
||||
}
|
||||
</div>;
|
||||
|
||||
const DeviceListItem: React.FC<{
|
||||
device: DeviceWithVerification;
|
||||
isExpanded: boolean;
|
||||
onDeviceExpandToggle: () => void;
|
||||
}> = ({
|
||||
device, isExpanded, onDeviceExpandToggle,
|
||||
}) => <li className='mx_FilteredDeviceList_listItem'>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
>
|
||||
<DeviceExpandDetailsButton
|
||||
isExpanded={isExpanded}
|
||||
onClick={onDeviceExpandToggle}
|
||||
/>
|
||||
</DeviceTile>
|
||||
{ isExpanded && <DeviceDetails device={device} /> }
|
||||
</li>;
|
||||
|
||||
/**
|
||||
* Filtered list of devices
|
||||
* Sorted by latest activity descending
|
||||
*/
|
||||
const FilteredDeviceList: React.FC<Props> = ({
|
||||
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 <div className='mx_FilteredDeviceList'>
|
||||
<div className='mx_FilteredDeviceList_header'>
|
||||
<span className='mx_FilteredDeviceList_headerLabel'>
|
||||
{ _t('Sessions') }
|
||||
</span>
|
||||
<Dropdown
|
||||
id='device-list-filter'
|
||||
label={_t('Filter devices')}
|
||||
value={filter || ALL_FILTER_ID}
|
||||
onOptionChange={onFilterOptionChange}
|
||||
>
|
||||
{ options.map(({ id, label }) =>
|
||||
<div data-test-id={`device-filter-option-${id}`} key={id}>{ label }</div>,
|
||||
) }
|
||||
</Dropdown>
|
||||
</div>
|
||||
{ !!sortedDevices.length
|
||||
? <FilterSecurityCard filter={filter} />
|
||||
: <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
|
||||
}
|
||||
<ol className='mx_FilteredDeviceList_list'>
|
||||
{ sortedDevices.map((device) => <DeviceListItem
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||
/>,
|
||||
) }
|
||||
</ol>
|
||||
</div>
|
||||
;
|
||||
};
|
||||
|
||||
export default FilteredDeviceList;
|
|
@ -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<Props> = ({ devices }) => {
|
||||
const devicesArray = Object.values<DeviceWithVerification>(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 <SettingsSubsection
|
||||
heading={_t('Security recommendations')}
|
||||
description={_t('Improve your account security by following these recommendations')}
|
||||
data-testid='security-recommendations-section'
|
||||
>
|
||||
{
|
||||
!!unverifiedDevicesCount &&
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Unverified}
|
||||
heading={_t('Unverified sessions')}
|
||||
description={_t(
|
||||
`Verify your sessions for enhanced secure messaging` +
|
||||
` or sign out from those you don't recognize or use anymore.`,
|
||||
)}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={noop}
|
||||
>
|
||||
{ _t('View all') + ` (${unverifiedDevicesCount})` }
|
||||
</AccessibleButton>
|
||||
</DeviceSecurityCard>
|
||||
}
|
||||
{
|
||||
!!inactiveDevicesCount &&
|
||||
<>
|
||||
{ !!unverifiedDevicesCount && <div className='mx_SecurityRecommendations_spacing' /> }
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Inactive}
|
||||
heading={_t('Inactive sessions')}
|
||||
description={_t(
|
||||
`Consider signing out from old sessions ` +
|
||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
||||
{ inactiveAgeDays },
|
||||
)}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={noop}
|
||||
>
|
||||
{ _t('View all') + ` (${inactiveDevicesCount})` }
|
||||
</AccessibleButton>
|
||||
</DeviceSecurityCard>
|
||||
</>
|
||||
}
|
||||
</SettingsSubsection>;
|
||||
};
|
||||
|
||||
export default SecurityRecommendations;
|
83
src/components/views/settings/devices/deleteDevices.tsx
Normal file
|
@ -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<void> => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
43
src/components/views/settings/devices/filter.ts
Normal file
|
@ -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, DeviceFilterCondition> = {
|
||||
[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)));
|
||||
};
|
26
src/components/views/settings/devices/types.ts
Normal file
|
@ -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<DeviceWithVerification['device_id'], DeviceWithVerification>;
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
Unverified = 'Unverified',
|
||||
Inactive = 'Inactive',
|
||||
}
|
105
src/components/views/settings/devices/useOwnDevices.ts
Normal file
|
@ -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<DevicesState['devices']> => {
|
||||
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<DevicesState['devices']>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<OwnDevicesError>();
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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<HTMLDivElement> {
|
||||
heading: string;
|
||||
description?: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({ heading, description, children }) => (
|
||||
<div className="mx_SettingsSubsection">
|
||||
const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({ heading, description, children, ...rest }) => (
|
||||
<div {...rest} className="mx_SettingsSubsection">
|
||||
<Heading className="mx_SettingsSubsection_heading" size='h3'>{ heading }</Heading>
|
||||
{ !!description && <div className="mx_SettingsSubsection_description">{ description }</div> }
|
||||
<div className="mx_SettingsSubsection_content">
|
||||
|
|
|
@ -46,6 +46,7 @@ interface IState {
|
|||
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private static ROOM_LIST_SETTINGS = [
|
||||
'breadcrumbs',
|
||||
"FTUE.userOnboardingButton",
|
||||
];
|
||||
|
||||
private static SPACES_SETTINGS = [
|
||||
|
|
|
@ -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<DeviceSecurityVariation>();
|
||||
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 <SettingsTab heading={_t('Sessions')}>
|
||||
<SettingsSubsection
|
||||
heading={_t('Current session')}
|
||||
// TODO session content coming here
|
||||
// in next PR
|
||||
<SecurityRecommendations devices={devices} />
|
||||
<CurrentDeviceSection
|
||||
device={currentDevice}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{
|
||||
shouldShowOtherSessions &&
|
||||
<SettingsSubsection
|
||||
heading={_t('Other sessions')}
|
||||
description={_t(
|
||||
`For best security, verify your sessions and sign out ` +
|
||||
`from any session that you don't recognize or use anymore.`,
|
||||
)}
|
||||
data-testid='other-sessions-section'
|
||||
>
|
||||
<FilteredDeviceList
|
||||
devices={otherDevices}
|
||||
filter={filter}
|
||||
expandedDeviceIds={expandedDeviceIds}
|
||||
onFilterChange={setFilter}
|
||||
onDeviceExpandToggle={onDeviceExpandToggle}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
}
|
||||
</SettingsTab>;
|
||||
};
|
||||
|
||||
|
|
|
@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut
|
|||
"collapsed": isPanelCollapsed,
|
||||
})}
|
||||
role="treeitem"
|
||||
aria-selected={selected}
|
||||
>
|
||||
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
|
||||
</li>;
|
||||
|
@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
|
|||
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<IInnerSpacePanelProps>(({
|
|||
|
||||
const SpacePanel = () => {
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
const ref = useRef<HTMLUListElement>();
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
useLayoutEffect(() => {
|
||||
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
||||
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||
|
@ -340,11 +344,9 @@ const SpacePanel = () => {
|
|||
}}>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<ul
|
||||
<div
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
role="tree"
|
||||
aria-label={_t("Spaces")}
|
||||
ref={ref}
|
||||
>
|
||||
<UserMenu isPanelCollapsed={isPanelCollapsed}>
|
||||
|
@ -381,7 +383,7 @@ const SpacePanel = () => {
|
|||
</Droppable>
|
||||
|
||||
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
||||
</ul>
|
||||
</div>
|
||||
) }
|
||||
</RovingTabIndexProvider>
|
||||
</DragDropContext>
|
||||
|
|
|
@ -315,6 +315,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { tabIndex, ...restDragHandleProps } = dragHandleProps || {};
|
||||
const selected = activeSpaces.includes(space.roomId);
|
||||
|
||||
return (
|
||||
<li
|
||||
|
@ -322,13 +323,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
className={itemClasses}
|
||||
ref={innerRef}
|
||||
aria-expanded={hasChildren ? !collapsed : undefined}
|
||||
aria-selected={selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<SpaceButton
|
||||
{...restDragHandleProps}
|
||||
space={space}
|
||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||
selected={activeSpaces.includes(space.roomId)}
|
||||
selected={selected}
|
||||
label={this.state.name}
|
||||
contextMenuTooltip={_t("Space options")}
|
||||
notificationState={notificationState}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
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, { useCallback } from "react";
|
||||
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { UseCase } from "../../../settings/enums/UseCase";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import Heading from "../../views/typography/Heading";
|
||||
import { showUserOnboardingPage } from "./UserOnboardingPage";
|
||||
|
||||
interface Props {
|
||||
selected: boolean;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export function UserOnboardingButton({ selected, minimized }: Props) {
|
||||
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
|
||||
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");
|
||||
|
||||
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserOnboardingButtonInternal selected={selected} minimized={minimized} />
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<AccessibleButton
|
||||
className={classNames("mx_UserOnboardingButton", {
|
||||
"mx_UserOnboardingButton_selected": selected,
|
||||
"mx_UserOnboardingButton_minimized": minimized,
|
||||
})}
|
||||
onClick={onClick}>
|
||||
{ !minimized && (
|
||||
<>
|
||||
<div className="mx_UserOnboardingButton_content">
|
||||
<Heading size="h4" className="mx_Heading_h4">
|
||||
{ _t("Welcome") }
|
||||
</Heading>
|
||||
<AccessibleButton
|
||||
className="mx_UserOnboardingButton_close"
|
||||
onClick={onDismiss}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
|
@ -32,10 +32,14 @@ export function UserOnboardingFeedback() {
|
|||
<div className="mx_UserOnboardingFeedback">
|
||||
<div className="mx_UserOnboardingFeedback_content">
|
||||
<Heading size="h4" className="mx_UserOnboardingFeedback_title">
|
||||
{ _t("How are you finding Element so far?") }
|
||||
{ _t("How are you finding %(brand)s so far?", {
|
||||
brand: SdkConfig.get("brand"),
|
||||
}) }
|
||||
</Heading>
|
||||
<div className="mx_UserOnboardingFeedback_text">
|
||||
{ _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"),
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
|
|
|
@ -62,7 +62,7 @@ export function UserOnboardingList({ completedTasks, waitingTasks }: Props) {
|
|||
</div>
|
||||
<ol className="mx_UserOnboardingList_list">
|
||||
{ tasks.map(([task, completed]) => (
|
||||
<UserOnboardingTask key={task.title} completed={completed} task={task} />
|
||||
<UserOnboardingTask key={task.id} completed={completed} task={task} />
|
||||
)) }
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -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<UseCase | null>("FTUE.useCaseSelection");
|
||||
const [completedTasks, waitingTasks] = useUserOnboardingTasks();
|
||||
const context = useUserOnboardingContext();
|
||||
const [completedTasks, waitingTasks] = useUserOnboardingTasks(context);
|
||||
|
||||
const initialSyncComplete = useInitialSyncComplete();
|
||||
const [showList, setShowList] = useState<boolean>(false);
|
||||
|
|
|
@ -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 (
|
||||
<li className={classNames("mx_UserOnboardingTask", {
|
||||
"mx_UserOnboardingTask_completed": completed,
|
||||
|
@ -42,10 +45,10 @@ export function UserOnboardingTask({ task, completed = false }: Props) {
|
|||
id={`mx_UserOnboardingTask_${task.id}`}
|
||||
className="mx_UserOnboardingTask_content">
|
||||
<Heading size="h4" className="mx_UserOnboardingTask_title">
|
||||
{ task.title }
|
||||
{ title }
|
||||
</Heading>
|
||||
<div className="mx_UserOnboardingTask_description">
|
||||
{ task.description }
|
||||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
{ task.action && (!task.action.hideOnComplete || !completed) && (
|
||||
|
|
|
@ -225,6 +225,10 @@ export function pickFactory(
|
|||
return noEventFactoryFactory(); // improper event type to render
|
||||
}
|
||||
|
||||
if (STATE_EVENT_TILE_TYPES[evType] === TextualEventFactory && !hasText(mxEvent, showHiddenEvents)) {
|
||||
return noEventFactoryFactory();
|
||||
}
|
||||
|
||||
return STATE_EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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<T extends any[], R>(value: (...values: T) => R): (...values: T) => R {
|
||||
const ref = useRef(value);
|
||||
ref.current = value;
|
||||
return useCallback(
|
||||
(...values: T) => ref.current(...values),
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: MatrixClient) => Promise<T>): T {
|
||||
const [value, setValue] = useState<T>(defaultValue);
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const handler = useRefOf(callback);
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
enabled = false;
|
||||
cli.off(ClientEvent.AccountData, repeater);
|
||||
if (handle !== null) {
|
||||
clearTimeout(handle);
|
||||
handle = null;
|
||||
}
|
||||
};
|
||||
}, [cli, handler, value]);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useUserOnboardingContext(): UserOnboardingContext | null {
|
||||
const [context, setContext] = useState<UserOnboardingContext | null>(null);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const handler = useCallback(async () => {
|
||||
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() ?? {};
|
||||
setContext({
|
||||
avatar: profile?.avatar_url ?? null,
|
||||
myDevice,
|
||||
devices: devices.devices,
|
||||
dmRooms: dmRooms,
|
||||
});
|
||||
}, [cli]);
|
||||
return Boolean(Object.keys(dmRooms).length);
|
||||
});
|
||||
const hasNotificationsEnabled = useUserOnboardingContextValue(false, async () => {
|
||||
return Notifier.isPossible();
|
||||
});
|
||||
|
||||
useEventEmitter(cli, ClientEvent.AccountData, handler);
|
||||
useEffect(() => {
|
||||
const handle = setInterval(handler, 2000);
|
||||
handler();
|
||||
return () => {
|
||||
if (handle) {
|
||||
clearInterval(handle);
|
||||
}
|
||||
};
|
||||
}, [handler]);
|
||||
|
||||
return context;
|
||||
return useMemo(
|
||||
() => ({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled }),
|
||||
[hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<UseCase | null>("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))];
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||
|
@ -2459,7 +2493,6 @@
|
|||
"We <Bold>don't</Bold> record or profile any account data": "We <Bold>don't</Bold> record or profile any account data",
|
||||
"We <Bold>don't</Bold> share information with third parties": "We <Bold>don't</Bold> 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",
|
||||
|
|
|
@ -3445,5 +3445,86 @@
|
|||
"Send your first message to invite <displayName/> to chat": "Envía tu primer mensaje para invitar a <displayName/> 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á!"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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 <a>server admin</a>.": "צרו קשר עם <a>מנהל השרת</a>.",
|
||||
"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 <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "אנא <newIssueLink> צור בעיה חדשה </newIssueLink> ב- 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 <a>Security Disclosure Policy</a>.": "כדי לדווח על בעיית אבטחה הקשורה למטריקס, אנא קראו את <a> מדיניות גילוי האבטחה של Matrix.org </a>.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "כדי לדווח על בעיית אבטחה , אנא קראו את <a> מדיניות גילוי האבטחה של Matrix.org </a>.",
|
||||
"Bug reporting": "דיווח על תקלות ובאגים",
|
||||
"Chat with %(brand)s Bot": "דבר עם הבוט של %(brand)s",
|
||||
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "לעזרה בשימוש ב-%(brand)s לחץ על <a> כאן </a> או התחל צ'אט עם הבוט שלנו באמצעות הלחצן למטה.",
|
||||
"For help with using %(brand)s, click <a>here</a>.": "בשביל לעזור בקידום ושימוש ב- %(brand)s, לחצו <a>כאן</a>.",
|
||||
"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 <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "טיפ למקצוענים: אם אתה מפעיל באג, שלח <debugLogsLink> יומני איתור באגים </debugLogsLink> כדי לעזור לנו לאתר את הבעיה.",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "אנא צפה תחילה ב <existingIssuesLink> באגים קיימים ב- Github </existingIssuesLink>. אין התאמה? <newIssueLink> התחל חדש </newIssueLink>.",
|
||||
|
@ -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: <networkLink/>": "סביבת עבודה: <networkLink/>",
|
||||
"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 <a>your settings</a> for more information": "אתחול חיפוש ההודעות נכשל. בדוק את <a>ההגדרות שלך</a> למידע נוסף",
|
||||
"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 <b>Downloads</b> folder.": "מפתח האבטחה שלך נמצא ב<b>תיקיית ההורדות</b> שלך.",
|
||||
"Confirm your Security Phrase": "אשר את ביטוי האבטחה שלך",
|
||||
"Secure your backup with a Security Phrase": "אבטח את הגיבוי שלך עם ביטוי אבטחה",
|
||||
"You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>": "אתה מנסה לגשת לקישור קהילה (%(groupId)s).<br/>קהילות כבר אינן נתמכות והוחלפו מרחבים.<br2/><a>למידע נוסף על מרחבים עיין כאן.</a>",
|
||||
"You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>": "אתם מנסים לגשת לקישור קהילה (%(groupId)s).<br/>קהילות כבר אינן נתמכות והוחלפו במרחבי עבודה.<br2/><a>למידע נוסף על מרחבי עבודה עיינו כאן.</a>",
|
||||
"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 <displayName/> to chat": "שילחו את ההודעה הראשונה שלכם להזמין את <displayName/> לצ'אט",
|
||||
"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 <name/>": "ברוכים הבאים אל <name/>",
|
||||
"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": "שימרו על דיונים מאורגנים בשרשורים",
|
||||
"<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.": "<b>טיפ:</b> השתמש ב-\"%(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. <a>Learn more</a>.": "השרת שלכם אינו תומך כעת בשרשורים, כך שתכונה זו עשויה להיות לא אמינה. ייתכן שהודעות שרשור מסוימות לא יהיו זמינות באופן מהימן. <a>למידע נוסף</a>.",
|
||||
"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. <a>Learn more</a>.": "שרשורים עוזרים לשמור על שיחות בנושא וקל למעקב. <a>למידע נוסף</a>.",
|
||||
"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": "הזמנה באמצעות שם משתמש",
|
||||
"<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>זוהי תכונה ניסיונית.</b> לעת עתה, משתמשים חדשים שמקבלים הזמנה יצטרכו לפתוח את ההזמנה ב-<link/> כדי להצטרף בפועל.",
|
||||
"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. <LearnMoreLink>Learn More</LearnMoreLink>": "שתף נתונים אנונימיים כדי לעזור לנו לזהות בעיות. ללא אישי. אין צדדים שלישיים. <LearnMoreLink>למידע נוסף</LearnMoreLink>",
|
||||
"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 <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "זאת התחלת ייצוא של <roomName/>. ייצוא ע\"י <exporterDetails/> ב %(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 <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "קיראו עוד ב<privacyPolicyLink />, <termsOfServiceLink />ו<cookiePolicyLink />.",
|
||||
"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. <a>Learn more</a>.": "מרגישים ניסיוניים? מעבדת הפיתוח היא הדרך הטובה ביותר לנסות פיתוחים חדשים לפני כולם, לבחון תכונות חדשות ולעזור לעצב אותן לפני שהן מושקות בפועל <a>למידע נוסף</a>.",
|
||||
"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, <a>click here</a>.": "כדי לצפות בכל קיצורי המקלדת , <a>ליחצו כאן</a>.",
|
||||
"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 <SpaceName/>.": "כל אחד יוכל למצוא ולהצטרך אל חלל עבודה זה. לא רק חברי <SpaceName/>.",
|
||||
"Anyone in <SpaceName/> will be able to find and join.": "כל אחד ב<SpaceName/> יוכל למצוא ולהצטרף.",
|
||||
"Visible to space members": "נראה לחברי מרחב העבודה",
|
||||
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "כל אחד יוכל למצוא ולהצטרך אל חדר זה, לא רק משתתפי מרחב עבודה <SpaceName/>.",
|
||||
"Everyone in <SpaceName/> will be able to find and join this room.": "כל אחד ב<SpaceName/> יוכל למצוא ולהצטרף אל חדר זה.",
|
||||
"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 <issueLink>submit a bug report</issueLink>.": "%(errcode)s הוחזר בעת ניסיון לגשת לחדר או למרחב העבודה. אם אתם חושבים שאתם רואים הודעה זו בטעות, אנא <issueLink>שילחו דוח באג</issueLink>.",
|
||||
"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 <spaceName/> can find and join. You can select other spaces too.": "כל אחד ב-<spaceName/> יכול למצוא ולהצטרף. אתם יכולים לבחור גם מרחבי עבודה אחרים.",
|
||||
"Spaces with access": "מרחבי עבודה עם גישה",
|
||||
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "כל אחד במרחב העבודה יכול למצוא ולהצטרף. <a>ערוך לאילו מרחבי עבודה יש גישה כאן.</a>",
|
||||
"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 <code>/plain</code> to send without markdown and <code>/md</code> to send with.": "התחילו הודעות עם <code>/plain</code> לשליחה ללא סימון ו-<code>/md</code> לשליחה.",
|
||||
"Get notified only with mentions and keywords as set up in your <a>settings</a>": "קבלו התראה רק עם אזכורים ומילות מפתח כפי שהוגדרו ב<a>הגדרות</a> שלכם",
|
||||
"New keyword": "מילת מפתח חדשה",
|
||||
"Keyword": "מילת מפתח"
|
||||
}
|
||||
|
|
|
@ -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 <img/>": "%(senderDisplayName)s megváltoztatta a szoba avatar képét: <img/>",
|
||||
"%(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 <img/>": "%(senderDisplayName)s megváltoztatta a szoba profilképét: <img/>",
|
||||
"%(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. <a>Learn more about encryption.</a>": "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. <a>Tudjon meg többet a titkosításról.</a>",
|
||||
"Power level": "Hozzáférési szint",
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> 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.": "<b>Figyelmeztetés</b>: A szoba frissítése <i>nem fogja automatikusan átvinni a szoba résztvevőit az új verziójú szobába.</i> 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 <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek <newIssueLink>készíts egy új hibajegyet</newIssueLink> a GitHubon.",
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Ahhoz hogy megvizsgálhassuk a hibát, <newIssueLink>hozzon létre egy új hibajegyet</newIssueLink> 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 <helpIcon /> with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> %(widgetDomain)s domain-nel.",
|
||||
"Room ID": "Szobaazonosító",
|
||||
"Widget ID": "Kisalkalmazás-azonosító",
|
||||
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> 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 <desktopLink>%(brand)s Desktop</desktopLink> 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 <desktopLink>Asztali %(brand)s</desktopLink> 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, <code>@bot:*</code> 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: <code>@bot:*</code> 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, <code>@bot:*</code> 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 <code>@bot:*</code> 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 <userId/>).": "Indíts beszélgetést valakivel és használd hozzá a nevét vagy a felhasználói nevét (mint <userId/>).",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Hívj meg valakit a nevét, vagy felhasználónevét (például <userId/>) megadva, vagy <a>oszd meg ezt a szobát</a>.",
|
||||
"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 <roomName/>.": "Ez a(z) <roomName/> 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 <helpIcon /> with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.",
|
||||
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> 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 <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Használjon Integrációs Menedzsert <b>(%(serverName)s)</b> 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 <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Használjon integrációkezelőt <b>(%(serverName)s)</b> 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 <SpaceName/> will be able to find and join this room.": "<SpaceName/> 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 <spaceName/> can find and join. You can select other spaces too.": "<spaceName/> 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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|