diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index ffc3b21181..1faffbbdf7 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -9,21 +9,17 @@ src/components/structures/UploadBar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/SetPasswordDialog.js -src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/elements/AddressSelector.js src/components/views/elements/DirectorySearchBox.js src/components/views/elements/MemberEventListSummary.js src/components/views/elements/UserSelector.js -src/components/views/globals/MatrixToolbar.js src/components/views/globals/NewVersionBar.js -src/components/views/globals/UpdateCheckBar.js src/components/views/messages/MFileBody.js src/components/views/messages/TextualBody.js src/components/views/room_settings/ColorSettings.js src/components/views/rooms/Autocomplete.js src/components/views/rooms/AuxPanel.js src/components/views/rooms/LinkPreviewWidget.js -src/components/views/rooms/MemberDeviceInfo.js src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberList.js src/components/views/rooms/RoomList.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e5515f1015..63702de38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,198 @@ +Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1) + + * Upgrade to JS SDK 6.2.1 + * Fix exceptions from Tooltip + [\#4716](https://github.com/matrix-org/matrix-react-sdk/pull/4716) + * Fix not being able to dismiss new login toasts + [\#4715](https://github.com/matrix-org/matrix-react-sdk/pull/4715) + * Fix compact layout regression + [\#4714](https://github.com/matrix-org/matrix-react-sdk/pull/4714) + +Changes in [2.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0) (2020-06-04) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.2...v2.7.0) + + * Upgrade to JS SDK 6.2.0 + * Prevent (double) 4S bootstrap from RestoreKeyBackupDialog + [\#4703](https://github.com/matrix-org/matrix-react-sdk/pull/4703) + * Fix checkbox bleed + [\#4702](https://github.com/matrix-org/matrix-react-sdk/pull/4702) + * Fix login loop where the sso flow returns to `#/login` to release + [\#4693](https://github.com/matrix-org/matrix-react-sdk/pull/4693) + +Changes in [2.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.2) (2020-06-02) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.1...v2.7.0-rc.2) + + * Rewire the Sticker button to be an Emoji Picker + [\#3747](https://github.com/matrix-org/matrix-react-sdk/pull/3747) + +Changes in [2.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.1) (2020-06-02) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.1...v2.7.0-rc.1) + + * Upgrade to JS SDK 6.2.0-rc.1 + * Update from Weblate + [\#4683](https://github.com/matrix-org/matrix-react-sdk/pull/4683) + * Make auth argument in the register request compliant with r0.6.0 + [\#4347](https://github.com/matrix-org/matrix-react-sdk/pull/4347) + * Revert "Prevent PersistedElements overflowing scrolled areas" + [\#4682](https://github.com/matrix-org/matrix-react-sdk/pull/4682) + * Remove unused TagPanelButtons + [\#4680](https://github.com/matrix-org/matrix-react-sdk/pull/4680) + * Pass roomId to IRCTimelineProfileResizer + [\#4679](https://github.com/matrix-org/matrix-react-sdk/pull/4679) + * Remove logging to console for irc name resize + [\#4678](https://github.com/matrix-org/matrix-react-sdk/pull/4678) + * Use arrow functions instead of binding `this` + [\#4677](https://github.com/matrix-org/matrix-react-sdk/pull/4677) + * Increase specificity of compact layout selectors + [\#4675](https://github.com/matrix-org/matrix-react-sdk/pull/4675) + * Create and use stylised checkboxes + [\#4665](https://github.com/matrix-org/matrix-react-sdk/pull/4665) + * useIRCLayout moved to props + [\#4676](https://github.com/matrix-org/matrix-react-sdk/pull/4676) + * Fix paste image to upload + [\#4674](https://github.com/matrix-org/matrix-react-sdk/pull/4674) + * Fix FilePanel and NotificationsPanel regression + [\#4647](https://github.com/matrix-org/matrix-react-sdk/pull/4647) + * Allow deferring of Update Toast until the next morning + [\#4669](https://github.com/matrix-org/matrix-react-sdk/pull/4669) + * Give contextual feedback for manual update check instead of banner + [\#4668](https://github.com/matrix-org/matrix-react-sdk/pull/4668) + * Dialog wrap title instead of taking same space as the close/cancel button + [\#4659](https://github.com/matrix-org/matrix-react-sdk/pull/4659) + * Update Modular hosting link + [\#4627](https://github.com/matrix-org/matrix-react-sdk/pull/4627) + * Fix field placeholder regression + [\#4663](https://github.com/matrix-org/matrix-react-sdk/pull/4663) + * Fix/document a number of UIA oddities + [\#4667](https://github.com/matrix-org/matrix-react-sdk/pull/4667) + * Stop copy icon repeating weirdly + [\#4662](https://github.com/matrix-org/matrix-react-sdk/pull/4662) + * Try and fix the Notifier race + [\#4661](https://github.com/matrix-org/matrix-react-sdk/pull/4661) + * set the client's pickle key if the platform can store one + [\#4657](https://github.com/matrix-org/matrix-react-sdk/pull/4657) + * Migrate Banners to Toasts + [\#4624](https://github.com/matrix-org/matrix-react-sdk/pull/4624) + * Move Appearance tab to ts + [\#4658](https://github.com/matrix-org/matrix-react-sdk/pull/4658) + * Fix room alias lookup vs peeking race condition + [\#4606](https://github.com/matrix-org/matrix-react-sdk/pull/4606) + * Fix encryption icon miss-alignment + [\#4651](https://github.com/matrix-org/matrix-react-sdk/pull/4651) + * Fix sublist sizing regression + [\#4649](https://github.com/matrix-org/matrix-react-sdk/pull/4649) + * Fix lines overflowing room list width + [\#4650](https://github.com/matrix-org/matrix-react-sdk/pull/4650) + * Remove the keyshare dialog + [\#4648](https://github.com/matrix-org/matrix-react-sdk/pull/4648) + * Update badge counts in new room list as needed + [\#4654](https://github.com/matrix-org/matrix-react-sdk/pull/4654) + * EventIndex: Handle invalid m.room.redaction events correctly. + [\#4653](https://github.com/matrix-org/matrix-react-sdk/pull/4653) + * EventIndex: Print out the checkpoint if there was an error during a crawl + [\#4652](https://github.com/matrix-org/matrix-react-sdk/pull/4652) + * Move Field to Typescript + [\#4635](https://github.com/matrix-org/matrix-react-sdk/pull/4635) + * Use connection error to detect network problem + [\#4646](https://github.com/matrix-org/matrix-react-sdk/pull/4646) + * Revert default font size to 15px + [\#4641](https://github.com/matrix-org/matrix-react-sdk/pull/4641) + * Add logging when room join fails + [\#4645](https://github.com/matrix-org/matrix-react-sdk/pull/4645) + * Remove EncryptedEventDialog + [\#4644](https://github.com/matrix-org/matrix-react-sdk/pull/4644) + * Migrate Toasts to Typescript and to granular priority system + [\#4618](https://github.com/matrix-org/matrix-react-sdk/pull/4618) + * Update Crypto Store Too New copy + [\#4632](https://github.com/matrix-org/matrix-react-sdk/pull/4632) + * MemberAvatar should not have its own letter fallback, it should use + BaseAvatar + [\#4643](https://github.com/matrix-org/matrix-react-sdk/pull/4643) + * Fix media upload issues with abort and status bar + [\#4630](https://github.com/matrix-org/matrix-react-sdk/pull/4630) + * fix viewGroup to actually show the group if possible + [\#4633](https://github.com/matrix-org/matrix-react-sdk/pull/4633) + * Update confirm passphrase copy + [\#4634](https://github.com/matrix-org/matrix-react-sdk/pull/4634) + * Improve accessibility of the emoji picker + [\#4636](https://github.com/matrix-org/matrix-react-sdk/pull/4636) + * Fix Emoji Picker footer being too small if text overflows + [\#4631](https://github.com/matrix-org/matrix-react-sdk/pull/4631) + * Improve style of toasts to match Figma + [\#4613](https://github.com/matrix-org/matrix-react-sdk/pull/4613) + * Iterate toast count indicator more logically + [\#4620](https://github.com/matrix-org/matrix-react-sdk/pull/4620) + * Fix reacting to redactions + [\#4626](https://github.com/matrix-org/matrix-react-sdk/pull/4626) + * Fix sentMessageAndIsAlone by dispatching `message_sent` more consistently + [\#4628](https://github.com/matrix-org/matrix-react-sdk/pull/4628) + * Update from Weblate + [\#4640](https://github.com/matrix-org/matrix-react-sdk/pull/4640) + * Replace `alias` with `address` in copy for consistency + [\#4402](https://github.com/matrix-org/matrix-react-sdk/pull/4402) + * Convert MatrixClientPeg to TypeScript + [\#4638](https://github.com/matrix-org/matrix-react-sdk/pull/4638) + * Fix BaseAvatar wrongly retrying urls + [\#4629](https://github.com/matrix-org/matrix-react-sdk/pull/4629) + * Fix event highlights not being updated to reflect edits + [\#4637](https://github.com/matrix-org/matrix-react-sdk/pull/4637) + * Calculate badges in the new room list more reliably + [\#4625](https://github.com/matrix-org/matrix-react-sdk/pull/4625) + * Transition BaseAvatar to hooks + [\#4101](https://github.com/matrix-org/matrix-react-sdk/pull/4101) + * Convert BasePlatform and BaseEventIndexManager to Typescript + [\#4614](https://github.com/matrix-org/matrix-react-sdk/pull/4614) + * Fix: Tag_DM is not defined + [\#4619](https://github.com/matrix-org/matrix-react-sdk/pull/4619) + * Fix visibility of message timestamps + [\#4615](https://github.com/matrix-org/matrix-react-sdk/pull/4615) + * Rewrite the room list store + [\#4253](https://github.com/matrix-org/matrix-react-sdk/pull/4253) + * Update code style to mention switch statements + [\#4610](https://github.com/matrix-org/matrix-react-sdk/pull/4610) + * Fix key backup restore with SSSS + [\#4612](https://github.com/matrix-org/matrix-react-sdk/pull/4612) + * Handle null tokens in the crawler loop. + [\#4608](https://github.com/matrix-org/matrix-react-sdk/pull/4608) + * Font scaling settings and slider + [\#4424](https://github.com/matrix-org/matrix-react-sdk/pull/4424) + * Prevent PersistedElements overflowing scrolled areas + [\#4494](https://github.com/matrix-org/matrix-react-sdk/pull/4494) + * IRC ui layout + [\#4531](https://github.com/matrix-org/matrix-react-sdk/pull/4531) + * Remove SSSS key upgrade check from rageshake + [\#4607](https://github.com/matrix-org/matrix-react-sdk/pull/4607) + * Label the create room button better than "Add room" + [\#4603](https://github.com/matrix-org/matrix-react-sdk/pull/4603) + * Convert the dispatcher to TypeScript + [\#4593](https://github.com/matrix-org/matrix-react-sdk/pull/4593) + * Consolidate password/passphrase fields into a component & add dynamic colour + to progress + [\#4599](https://github.com/matrix-org/matrix-react-sdk/pull/4599) + * UserView, show Welcome page in the mid panel instead of empty space + [\#4590](https://github.com/matrix-org/matrix-react-sdk/pull/4590) + * Update from Weblate + [\#4601](https://github.com/matrix-org/matrix-react-sdk/pull/4601) + * Make email auth component fail better if server claims email isn't validated + [\#4600](https://github.com/matrix-org/matrix-react-sdk/pull/4600) + * Add new keyboard shortcuts for jump to unread and upload file + [\#4588](https://github.com/matrix-org/matrix-react-sdk/pull/4588) + * accept and linkify local domains like those from mDNS + [\#4594](https://github.com/matrix-org/matrix-react-sdk/pull/4594) + * Revert "ImageView make clicking off it easier" + [\#4586](https://github.com/matrix-org/matrix-react-sdk/pull/4586) + * wrap node-qrcode in a React FC and use it for ShareDialog + [\#4394](https://github.com/matrix-org/matrix-react-sdk/pull/4394) + * Pass screenAfterLogin through SSO in the callback url + [\#4585](https://github.com/matrix-org/matrix-react-sdk/pull/4585) + * Remove debugging that causes email addresses to load forever + [\#4597](https://github.com/matrix-org/matrix-react-sdk/pull/4597) + Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1) diff --git a/code_style.md b/code_style.md index 01c1f37146..fe04d2cc3d 100644 --- a/code_style.md +++ b/code_style.md @@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide The intention of this guide is to make Matrix's JavaScript codebase clean, consistent with other popular JavaScript styles and consistent with the rest of the Matrix codebase. For reference, the Matrix Python style guide can be found -at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst +at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md This document reflects how we would like Matrix JavaScript code to look, with acknowledgement that a significant amount of code is written to older @@ -17,7 +17,7 @@ writing in modern ECMAScript and using a transpile step to generate the file that applications can then include. There are significant benefits in being able to use modern ECMAScript, although the tooling for doing so can be awkward for library code, especially with regard to translating source maps and line -number throgh from the original code to the final application. +number through from the original code to the final application. General Style ------------- diff --git a/package.json b/package.json index 1783102b35..93d59a4fa6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.6.1", + "version": "2.7.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -65,8 +65,8 @@ "create-react-class": "^15.6.0", "diff-dom": "^4.1.3", "diff-match-patch": "^1.0.4", - "emojibase-data": "^4.0.2", - "emojibase-regex": "^3.0.0", + "emojibase-data": "^5.0.1", + "emojibase-regex": "^4.0.1", "escape-html": "^1.0.3", "file-saver": "^1.3.3", "filesize": "3.5.6", @@ -93,6 +93,7 @@ "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", "react-focus-lock": "^2.2.1", + "react-resizable": "^1.10.1", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", "text-encoding-utf-8": "^1.0.1", @@ -119,9 +120,12 @@ "@peculiar/webcrypto": "^1.0.22", "@types/classnames": "^2.2.10", "@types/flux": "^3.1.9", + "@types/lodash": "^4.14.152", "@types/modernizr": "^3.5.3", + "@types/node": "^12.12.41", "@types/qrcode": "^1.3.4", - "@types/react": "16.9", + "@types/react": "^16.9", + "@types/react-dom": "^16.9.8", "@types/zxcvbn": "^4.4.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", @@ -161,7 +165,9 @@ "testMatch": [ "/test/**/*-test.js" ], - "setupFiles": ["jest-canvas-mock"], + "setupFiles": [ + "jest-canvas-mock" + ], "setupFilesAfterEnv": [ "/test/setupTests.js" ], diff --git a/res/css/_common.scss b/res/css/_common.scss index 03442ca510..ebeeb381e6 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -335,6 +335,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title { text-align: center; } +.mx_Dialog_header.mx_Dialog_headerWithCancel > .mx_Dialog_title { + margin-right: 20px; // leave space for the 'X' cancel button +} .mx_Dialog_title.danger { color: $warning-color; diff --git a/res/css/_components.scss b/res/css/_components.scss index 0a93feb787..6995795117 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -61,9 +61,7 @@ @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; -@import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; -@import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @@ -82,7 +80,6 @@ @import "./views/dialogs/_SlashCommandHelpDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; -@import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @@ -117,6 +114,7 @@ @import "./views/elements/_RoomAliasField.scss"; @import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; +@import "./views/elements/_StyledCheckbox.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @@ -124,7 +122,6 @@ @import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; @import "./views/emojipicker/_EmojiPicker.scss"; -@import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; @@ -169,7 +166,6 @@ @import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; -@import "./views/rooms/_MemberDeviceInfo.scss"; @import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @@ -185,6 +181,7 @@ @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss"; +@import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @@ -205,6 +202,7 @@ @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/_SetIdServer.scss"; @import "./views/settings/_SetIntegrationManager.scss"; +@import "./views/settings/_UpdateCheckButton.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index 76a9b16425..2d7ab67e40 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -15,6 +15,7 @@ limitations under the License. */ $font-1px: 0.067rem; +$font-1-5px: 0.100rem; $font-2px: 0.133rem; $font-3px: 0.200rem; $font-4px: 0.267rem; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d57425f6f..899824bc57 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -19,9 +19,18 @@ limitations under the License. display: flex; /* LeftPanel 260px */ min-width: 260px; + max-width: 50%; flex: 0 0 auto; } +// TODO: Remove temporary indicator of new room list implementation. +// This border is meant to visually distinguish between the two components when the +// user has turned on the new room list implementation, at least until the designs +// themselves give it away. +.mx_LeftPanel2 .mx_LeftPanel { + border-left: 5px #e26dff solid; +} + .mx_LeftPanel_container.collapsed { min-width: unset; /* Collapsed LeftPanel 50px */ diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index c5a5d50068..05c703ab6d 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -41,10 +41,6 @@ limitations under the License. height: 40px; } -.mx_MatrixChat_toolbarShowing { - height: auto; -} - .mx_MatrixChat { width: 100%; height: 100%; diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 44205b1f01..561ab1446f 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -63,6 +63,10 @@ limitations under the License. padding-left: 32px; padding-top: 8px; position: relative; + + a { + display: flex; + } } .mx_NotificationPanel .mx_EventTile_roomName a, diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 10878322e3..600871e071 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -20,6 +20,7 @@ limitations under the License. flex: 0 0 auto; position: relative; min-width: 264px; + max-width: 50%; display: flex; flex-direction: column; } @@ -67,22 +68,27 @@ limitations under the License. .mx_RightPanel_membersButton::before { mask-image: url('$(res)/img/feather-customised/user.svg'); + mask-position: center; } .mx_RightPanel_filesButton::before { mask-image: url('$(res)/img/feather-customised/files.svg'); + mask-position: center; } .mx_RightPanel_notifsButton::before { mask-image: url('$(res)/img/feather-customised/notifications.svg'); + mask-position: center; } .mx_RightPanel_groupMembersButton::before { mask-image: url('$(res)/img/icons-people.svg'); + mask-position: center; } .mx_RightPanel_roomsButton::before { mask-image: url('$(res)/img/icons-room-nobg.svg'); + mask-position: center; } .mx_RightPanel_headerButton_highlight::after { diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 6ec4a0d152..2916c4ffdc 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,8 +28,8 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: white; - box-shadow: 0px 4px 12px $menu-box-shadow-color; + background-color: $dark-panel-bg-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,16 +37,15 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $primary-bg-color; - box-shadow: 0px 4px 12px $menu-box-shadow-color; + background-color: $dark-panel-bg-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; display: grid; - grid-template-columns: 20px 1fr; - column-gap: 10px; + grid-template-columns: 22px 1fr; + column-gap: 8px; row-gap: 4px; padding: 8px; - padding-right: 16px; &.mx_Toast_hasIcon { &::after { @@ -68,17 +67,45 @@ limitations under the License. background-image: url("$(res)/img/e2e/warning.svg"); } - h2, .mx_Toast_body { + .mx_Toast_title, .mx_Toast_body { grid-column: 2; } } + &:not(.mx_Toast_hasIcon) { + padding-left: 12px; - h2 { - grid-column: 1 / 3; - grid-row: 1; - margin: 0; - font-size: $font-15px; - font-weight: 600; + .mx_Toast_title { + grid-column: 1 / -1; + } + } + + .mx_Toast_title, + .mx_Toast_description { + padding-right: 8px; + } + + .mx_Toast_title { + width: 100%; + box-sizing: border-box; + + h2 { + grid-column: 1 / 3; + grid-row: 1; + margin: 0; + font-size: $font-15px; + font-weight: 600; + display: inline; + width: auto; + vertical-align: middle; + } + + span { + padding-left: 8px; + float: right; + font-size: $font-12px; + line-height: $font-22px; + color: $muted-fg-color; + } } .mx_Toast_body { @@ -87,7 +114,13 @@ limitations under the License. } .mx_Toast_buttons { + float: right; display: flex; + + .mx_FormButton { + min-width: 96px; + box-sizing: border-box; + } } .mx_Toast_description { @@ -96,6 +129,15 @@ limitations under the License. text-overflow: ellipsis; margin: 4px 0 11px 0; font-size: $font-12px; + + .mx_AccessibleButton_kind_link { + font-size: inherit; + padding: 0; + } + + a { + text-decoration: none; + } } .mx_Toast_deviceID { diff --git a/res/css/views/dialogs/_GroupAddressPicker.scss b/res/css/views/dialogs/_GroupAddressPicker.scss index 20a7cc1047..5fa18931f0 100644 --- a/res/css/views/dialogs/_GroupAddressPicker.scss +++ b/res/css/views/dialogs/_GroupAddressPicker.scss @@ -18,8 +18,3 @@ limitations under the License. margin-top: 10px; display: flex; } - -.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] { - /* Stop flex from shrinking the checkbox */ - width: 20px; -} diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index e08469ec6d..e3d2ae8306 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -55,6 +55,7 @@ limitations under the License. margin-left: 5px; width: 20px; height: 20px; + background-repeat: none; } .mx_ShareDialog_split { diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss deleted file mode 100644 index daa6bd2352..0000000000 --- a/res/css/views/dialogs/_UnknownDeviceDialog.scss +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -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_UnknownDeviceDialog { - height: 100%; - display: flex; - flex-direction: column; -} - -.mx_UnknownDeviceDialog ul { - list-style: none; - padding: 0; -} -// userid -.mx_UnknownDeviceDialog p { - font-weight: bold; - font-size: $font-16px; -} - -.mx_UnknownDeviceDialog .mx_DeviceVerifyButtons { - flex-direction: row !important; -} - -.mx_UnknownDeviceDialog .mx_Dialog_content { - margin-bottom: 24px; - overflow-y: scroll; -} - -.mx_UnknownDeviceDialog_deviceList > li { - padding: 4px; -} - -.mx_UnknownDeviceDialog_deviceList > li > * { - padding-bottom: 0; -} diff --git a/res/css/views/elements/_AddressSelector.scss b/res/css/views/elements/_AddressSelector.scss index dd78fcc0f0..087504390c 100644 --- a/res/css/views/elements/_AddressSelector.scss +++ b/res/css/views/elements/_AddressSelector.scss @@ -23,6 +23,7 @@ limitations under the License. border-radius: 3px; border: solid 1px $accent-color; cursor: pointer; + z-index: 1; } .mx_AddressSelector.mx_AddressSelector_empty { diff --git a/res/css/views/elements/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss new file mode 100644 index 0000000000..14081f1e99 --- /dev/null +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -0,0 +1,66 @@ +/* +Copyright 2020 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_Checkbox { + $size: $font-16px; + $border-size: $font-1-5px; + $border-radius: $font-4px; + + display: flex; + align-items: flex-start; + + input[type=checkbox] { + display: none; + + & + label { + display: flex; + align-items: center; + + flex-grow: 1; + } + + & + label > .mx_Checkbox_background { + display: inline-flex; + position: relative; + + flex-shrink: 0; + + height: $size; + width: $size; + size: 0.5rem; + + border: $border-size solid rgba($muted-fg-color, 0.5); + box-sizing: border-box; + border-radius: $border-radius; + + img { + height: 100%; + width: 100%; + filter: invert(100%); + } + } + + &:checked + label > .mx_Checkbox_background { + background: $accent-color; + border-color: $accent-color; + } + + & + label > *:not(.mx_Checkbox_background) { + margin-left: 10px; + } + } +} diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 24561eeeb9..400e40e233 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -190,7 +190,7 @@ limitations under the License. .mx_EmojiPicker_footer { border-top: 1px solid $message-action-bar-border-color; - height: 72px; + min-height: 72px; display: flex; align-items: center; diff --git a/res/css/views/globals/_MatrixToolbar.scss b/res/css/views/globals/_MatrixToolbar.scss deleted file mode 100644 index 5fdf572f99..0000000000 --- a/res/css/views/globals/_MatrixToolbar.scss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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_MatrixToolbar { - background-color: $accent-color; - color: $accent-fg-color; - - display: flex; - align-items: center; -} - -.mx_MatrixToolbar_warning { - margin-left: 16px; - margin-right: 8px; - margin-top: -2px; -} - -.mx_MatrixToolbar_info { - padding-left: 16px; - padding-right: 8px; - background-color: $info-bg-color; -} - -.mx_MatrixToolbar_error { - padding-left: 16px; - padding-right: 8px; - background-color: $warning-bg-color; -} - -.mx_MatrixToolbar_content { - flex: 1; -} - -.mx_MatrixToolbar_link { - color: $accent-fg-color !important; - text-decoration: underline !important; - cursor: pointer; -} - -.mx_MatrixToolbar_clickable { - cursor: pointer; -} - -.mx_MatrixToolbar_close { - cursor: pointer; -} - -.mx_MatrixToolbar_close img { - display: block; - float: right; - margin-right: 10px; -} - -.mx_MatrixToolbar_action { - margin-right: 16px; -} - -.mx_MatrixToolbar_changelog { - white-space: pre; -} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index e4743f189e..1b1bab67bc 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -96,10 +96,6 @@ $AppsDrawerBodyHeight: 273px; height: $AppsDrawerBodyHeight; } -.mx_AppTile_persistedWrapper > div { - height: 100%; -} - .mx_AppTile_mini .mx_AppTile_persistedWrapper { height: 114px; } diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 928ea75a79..de4a5538cd 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -18,7 +18,6 @@ limitations under the License. $left-gutter: 65px; .mx_GroupLayout { - .mx_EventTile { > .mx_SenderProfile { line-height: $font-17px; @@ -55,77 +54,77 @@ $left-gutter: 65px; .mx_MatrixChat_useCompactLayout { .mx_EventTile { padding-top: 4px; - } - .mx_EventTile.mx_EventTile_info { - // same as the padding for non-compact .mx_EventTile.mx_EventTile_info - padding-top: 0px; - font-size: $font-13px; .mx_EventTile_line, .mx_EventTile_reply { - line-height: $font-20px; + padding-top: 0; + padding-bottom: 0; } - .mx_EventTile_avatar { - top: 4px; + + &.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + padding-top: 0px; + font-size: $font-13px; + .mx_EventTile_line, .mx_EventTile_reply { + line-height: $font-20px; + } + .mx_EventTile_avatar { + top: 4px; + } } - } - .mx_EventTile .mx_SenderProfile { - font-size: $font-13px; - } + .mx_SenderProfile { + font-size: $font-13px; + } + + &.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide + padding-top: 8px; + .mx_EventTile_avatar { + top: 2px; + } + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + &.mx_EventTile_emote.mx_EventTile_continuation { + padding-top: 0; + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + } - .mx_EventTile.mx_EventTile_emote { - // add a bit more space for emotes so that avatars don't collide - padding-top: 8px; .mx_EventTile_avatar { top: 2px; } - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 1px; + + .mx_EventTile_e2eIcon { + top: 3px; } - } - .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { - padding-top: 0; - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; + .mx_EventTile_readAvatars { + top: 27px; } - } - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } + .mx_EventTile_continuation .mx_EventTile_readAvatars, + .mx_EventTile_emote .mx_EventTile_readAvatars { + top: 5px; + } - .mx_EventTile_avatar { - top: 2px; - } + .mx_EventTile_info .mx_EventTile_readAvatars { + top: 4px; + } - .mx_EventTile_e2eIcon { - top: 3px; - } - - .mx_EventTile_readAvatars { - top: 27px; - } - - .mx_EventTile_continuation .mx_EventTile_readAvatars, - .mx_EventTile_emote .mx_EventTile_readAvatars { - top: 5px; - } - - .mx_EventTile_info .mx_EventTile_readAvatars { - top: 4px; + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } } .mx_RoomView_MessageList h2 { margin-top: 6px; } - - .mx_EventTile_content .markdown-body { - p, ul, ol, dl, blockquote, pre, table { - margin-bottom: 4px; // 1/4 of the non-compact margin-bottom - } - } } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 5f88473c5f..a8eb35eeed 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -41,7 +41,7 @@ $irc-line-height: $font-18px; } > .mx_EventTile_msgOption { - order: 4; + order: 5; flex-shrink: 0; } @@ -63,6 +63,8 @@ $irc-line-height: $font-18px; flex-direction: column; order: 3; flex-grow: 1; + flex-shrink: 1; + min-width: 0; } > .mx_EventTile_avatar { @@ -90,12 +92,14 @@ $irc-line-height: $font-18px; text-align: right; } - .mx_EventTile_e2eIcon { + > .mx_EventTile_e2eIcon { position: relative; right: unset; left: unset; - top: -2px; padding: 0; + order: 3; + flex-shrink: 0; + flex-grow: 0; } .mx_EventTile_line { @@ -113,7 +117,7 @@ $irc-line-height: $font-18px; } .mx_EventTile_reply { - order: 3; + order: 4; } .mx_EditMessageComposer_buttons { diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss deleted file mode 100644 index 71b05a93fc..0000000000 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -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_MemberDeviceInfo { - display: flex; - padding-bottom: 10px; - align-items: flex-start; -} - -.mx_MemberDeviceInfo_icon { - margin-top: 4px; - width: 12px; - height: 12px; - mask-repeat: no-repeat; - mask-size: 100%; -} -.mx_MemberDeviceInfo_icon_blacklisted { - mask-image: url('$(res)/img/e2e/blacklisted.svg'); - background-color: $warning-color; -} -.mx_MemberDeviceInfo_icon_verified { - mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $accent-color; -} -.mx_MemberDeviceInfo_icon_unverified { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; -} - -.mx_MemberDeviceInfo > .mx_DeviceVerifyButtons { - display: flex; - flex-direction: column; - flex: 0 1 auto; - align-items: stretch; -} - -.mx_MemberDeviceInfo_textButton { - @mixin mx_DialogButton_small; - margin: 2px; - flex: 1; -} - -.mx_MemberDeviceInfo_textButton:hover { - @mixin mx_DialogButton_hover; -} - -.mx_MemberDeviceInfo_deviceId { - word-break: break-word; - font-size: $font-13px; -} - -.mx_MemberDeviceInfo_deviceInfo { - margin: 0 5px 5px 8px; - flex: 1; -} - -/* "Unblacklist" is too long for a regular button: make it wider and - reduce the padding. */ -.mx_EncryptedEventDialog .mx_MemberDeviceInfo_blacklist, -.mx_EncryptedEventDialog .mx_MemberDeviceInfo_unblacklist { - padding-left: 1em; - padding-right: 1em; -} - -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified, -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified, -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted { - float: right; - padding-left: 1em; -} - -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified { - color: $e2e-verified-color; -} - -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified { - color: $e2e-unverified-color; -} - -.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted { - color: $e2e-warning-color; -} diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 7b223be3a4..c1cda7bf24 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -214,8 +214,12 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/video.svg'); } +.mx_MessageComposer_emoji::before { + mask-image: url('$(res)/img/feather-customised/emoji3.custom.svg'); +} + .mx_MessageComposer_stickers::before { - mask-image: url('$(res)/img/feather-customised/face.svg'); + mask-image: url('$(res)/img/feather-customised/sticker.custom.svg'); } .mx_MessageComposer_formatting { diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 50a9e7ee1f..c23c19699d 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -15,6 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_RoomList.mx_RoomList2 { + overflow-y: auto; +} + .mx_RoomList { /* take up remaining space below TopLeftMenu */ flex: 1; diff --git a/res/css/views/dialogs/_DeviceVerifyDialog.scss b/res/css/views/rooms/_RoomSublist2.scss similarity index 63% rename from res/css/views/dialogs/_DeviceVerifyDialog.scss rename to res/css/views/rooms/_RoomSublist2.scss index 1997e0c21d..9ab1785566 100644 --- a/res/css/views/dialogs/_DeviceVerifyDialog.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd. +Copyright 2020 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. @@ -14,16 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_DeviceVerifyDialog_cryptoSection ul { - display: table; -} +@import "../../../../node_modules/react-resizable/css/styles.css"; -.mx_DeviceVerifyDialog_cryptoSection li { - display: table-row; -} - -.mx_DeviceVerifyDialog_cryptoSection label, -.mx_DeviceVerifyDialog_cryptoSection span { - display: table-cell; - padding-right: 1em; +.mx_RoomList2 .mx_RoomSubList_labelContainer { + z-index: 12; } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 759dce5afa..7f93da0bbf 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -20,7 +20,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: 32px; + height: 34px; margin: 0; padding: 0 8px 0 10px; position: relative; diff --git a/res/css/views/settings/_UpdateCheckButton.scss b/res/css/views/settings/_UpdateCheckButton.scss new file mode 100644 index 0000000000..f35a023ac1 --- /dev/null +++ b/res/css/views/settings/_UpdateCheckButton.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020 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_UpdateCheckButton_summary { + margin-left: 16px; + + .mx_AccessibleButton_kind_link { + padding: 0; + } +} diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss index 8700f8747d..d6466a03f9 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss @@ -63,4 +63,25 @@ limitations under the License. font-size: inherit; } } + + .mx_SecurityUserSettingsTab_warning { + color: $notice-primary-color; + position: relative; + padding-left: 40px; + margin-top: 30px; + + &::before { + mask-repeat: no-repeat; + mask-position: 0 center; + mask-size: $font-24px; + position: absolute; + width: $font-24px; + height: $font-24px; + content: ""; + top: 0; + left: 0; + background-color: $notice-primary-color; + mask-image: url('$(res)/img/feather-customised/alert-triangle.svg'); + } + } } diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 index 593d7c8f5c..a52e5a3800 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 index 277324851f..660a93193d 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ diff --git a/res/img/feather-customised/alert-triangle.svg b/res/img/feather-customised/alert-triangle.svg new file mode 100644 index 0000000000..ceb664790f --- /dev/null +++ b/res/img/feather-customised/alert-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/feather-customised/emoji3.custom.svg b/res/img/feather-customised/emoji3.custom.svg new file mode 100644 index 0000000000..d91ba1c132 --- /dev/null +++ b/res/img/feather-customised/emoji3.custom.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/feather-customised/sticker.custom.svg b/res/img/feather-customised/sticker.custom.svg new file mode 100644 index 0000000000..691e3b3925 --- /dev/null +++ b/res/img/feather-customised/sticker.custom.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/file.png b/res/img/file.png deleted file mode 100644 index 5904ea8284..0000000000 Binary files a/res/img/file.png and /dev/null differ diff --git a/res/img/files.png b/res/img/files.png deleted file mode 100644 index 83932267f8..0000000000 Binary files a/res/img/files.png and /dev/null differ diff --git a/res/img/files.svg b/res/img/files.svg deleted file mode 100644 index 20aba851ea..0000000000 --- a/res/img/files.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - icons_browse_files - Created with bin/sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/@types/common.ts b/src/@types/common.ts new file mode 100644 index 0000000000..26e5317aa3 --- /dev/null +++ b/src/@types/common.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 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. +*/ + +// Based on https://stackoverflow.com/a/53229857/3532235 +export type Without = {[P in Exclude] ? : never} +export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index e6e339d067..ffd3277892 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,13 +15,24 @@ limitations under the License. */ import * as ModernizrStatic from "modernizr"; +import ContentMessages from "../ContentMessages"; +import { IMatrixClientPeg } from "../MatrixClientPeg"; +import ToastStore from "../stores/ToastStore"; +import DeviceListener from "../DeviceListener"; +import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; declare global { interface Window { Modernizr: ModernizrStatic; + mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; }; + + mx_ContentMessages: ContentMessages; + mx_ToastStore: ToastStore; + mx_DeviceListener: DeviceListener; + mx_RoomListStore2: RoomListStore2; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 diff --git a/src/Avatar.js b/src/Avatar.js index 8393ce02b2..2cb90eaea6 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +// Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember(member, width, height, resizeMethod) { let url; if (member && member.getAvatarUrl) { diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index d4a6c34daf..520c3fbe46 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -21,6 +21,22 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; import {ActionPayload} from "./dispatcher/payloads"; +import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload"; +import {Action} from "./dispatcher/actions"; +import {hideToast as hideUpdateToast} from "./toasts/UpdateToast"; + +export const HOMESERVER_URL_KEY = "mx_hs_url"; +export const ID_SERVER_URL_KEY = "mx_is_url"; + +export enum UpdateCheckStatus { + Checking = "CHECKING", + Error = "ERROR", + NotAvailable = "NOTAVAILABLE", + Downloading = "DOWNLOADING", + Ready = "READY", +} + +const UPDATE_DEFER_KEY = "mx_defer_update"; /** * Base class for classes that provide platform-specific functionality @@ -34,6 +50,7 @@ export default abstract class BasePlatform { constructor() { dis.register(this.onAction); + this.startUpdateCheck = this.startUpdateCheck.bind(this); } protected onAction = (payload: ActionPayload) => { @@ -56,6 +73,53 @@ export default abstract class BasePlatform { this.errorDidOccur = errorDidOccur; } + /** + * Whether we can call checkForUpdate on this platform build + */ + async canSelfUpdate(): Promise { + return false; + } + + startUpdateCheck() { + hideUpdateToast(); + localStorage.removeItem(UPDATE_DEFER_KEY); + dis.dispatch({ + action: Action.CheckUpdates, + status: UpdateCheckStatus.Checking, + }); + } + + /** + * Update the currently running app to the latest available version + * and replace this instance of the app with the new version. + */ + installUpdate() { + } + + /** + * Check if the version update has been deferred and that deferment is still in effect + * @param newVersion the version string to check + */ + protected shouldShowUpdate(newVersion: string): boolean { + try { + const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY)); + return newVersion !== version || Date.now() > deferUntil; + } catch (e) { + return true; + } + } + + /** + * Ignore the pending update and don't prompt about this version + * until the next morning (8am). + */ + deferUpdate(newVersion: string) { + const date = new Date(Date.now() + 24 * 60 * 60 * 1000); + date.setHours(8, 0, 0, 0); // set to next 8am + localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()])); + hideUpdateToast(); + } + /** * Returns true if the platform supports displaying * notifications, otherwise false. @@ -157,11 +221,9 @@ export default abstract class BasePlatform { setLanguage(preferredLangs: string[]) {} - getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL { + getSSOCallbackUrl(fragmentAfterLogin: string): URL { const url = new URL(window.location.href); url.hash = fragmentAfterLogin || ""; - url.searchParams.set("homeserver", hsUrl); - url.searchParams.set("identityServer", isUrl); return url; } @@ -172,12 +234,47 @@ export default abstract class BasePlatform { * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. */ startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { - const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(), - fragmentAfterLogin); + // persist hs url and is url for when the user is returned to the app with the login token + localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); + if (mxClient.getIdentityServerUrl()) { + localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); + } + const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO } onKeyDown(ev: KeyboardEvent): boolean { return false; // no shortcuts implemented } + + /** + * Get a previously stored pickle key. The pickle key is used for + * encrypting libolm objects. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} userId the device ID that the pickle key is for. + * @returns {string|null} the previously stored pickle key, or null if no + * pickle key has been stored. + */ + async getPickleKey(userId: string, deviceId: string): Promise { + return null; + } + + /** + * Create and store a pickle key for encrypting libolm objects. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} userId the device ID that the pickle key is for. + * @returns {string|null} the pickle key, or null if the platform does not + * support storing pickle keys. + */ + async createPickleKey(userId: string, deviceId: string): Promise { + return null; + } + + /** + * Delete a previously stored pickle key from storage. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} userId the device ID that the pickle key is for. + */ + async destroyPickleKey(userId: string, deviceId: string): Promise { + } } diff --git a/src/CallHandler.js b/src/CallHandler.js index c95ed16eb3..4414bce457 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -60,7 +60,6 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher/dispatcher'; -import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; @@ -119,62 +118,22 @@ function pause(audioId) { } } -function _reAttemptCall(call) { - if (call.direction === 'outbound') { - dis.dispatch({ - action: 'place_call', - room_id: call.roomId, - type: call.type, - }); - } else { - call.answer(); - } -} - function _setCallListeners(call) { call.on("error", function(err) { console.error("Call error:", err); - if (err.code === 'unknown_devices') { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { - title: _t('Call Failed'), - description: _t( - "There are unknown sessions in this room: "+ - "if you proceed without verifying them, it will be "+ - "possible for someone to eavesdrop on your call.", - ), - button: _t('Review Sessions'), - onFinished: function(confirmed) { - if (confirmed) { - const room = MatrixClientPeg.get().getRoom(call.roomId); - showUnknownDeviceDialogForCalls( - MatrixClientPeg.get(), - room, - () => { - _reAttemptCall(call); - }, - call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"), - call.direction === 'outbound' ? _t("Call") : _t("Answer"), - ); - } - }, - }); - } else { - if ( - MatrixClientPeg.get().getTurnServers().length === 0 && - SettingsStore.getValue("fallbackICEServerAllowed") === null - ) { - _showICEFallbackPrompt(); - return; - } - - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { - title: _t('Call Failed'), - description: err.message, - }); + if ( + MatrixClientPeg.get().getTurnServers().length === 0 && + SettingsStore.getValue("fallbackICEServerAllowed") === null + ) { + _showICEFallbackPrompt(); + return; } + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Call Failed'), + description: err.message, + }); }); call.on("hangup", function() { _setCallState(undefined, call.roomId, "ended"); diff --git a/src/ContentMessages.js b/src/ContentMessages.tsx similarity index 73% rename from src/ContentMessages.js rename to src/ContentMessages.tsx index 4f5a1a1220..25445b1c74 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.tsx @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 New Vector Ltd +Copyright 2020 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. @@ -15,20 +16,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - +import React from "react"; import extend from './extend'; import dis from './dispatcher/dispatcher'; import {MatrixClientPeg} from './MatrixClientPeg'; +import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import RoomViewStore from './stores/RoomViewStore'; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; +import Spinner from "./components/views/elements/Spinner"; // Polyfill for Canvas.toBlob API using Canvas.toDataURL import "blueimp-canvas-to-blob"; +import { Action } from "./dispatcher/actions"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -39,6 +42,50 @@ const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; export class UploadCanceledError extends Error {} +type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; + +interface IUpload { + fileName: string; + roomId: string; + total: number; + loaded: number; + promise: Promise; + canceled?: boolean; +} + +interface IMediaConfig { + "m.upload.size"?: number; +} + +interface IContent { + body: string; + msgtype: string; + info: { + size: number; + mimetype?: string; + }; + file?: string; + url?: string; +} + +interface IThumbnail { + info: { + thumbnail_info: { + w: number; + h: number; + mimetype: string; + size: number; + }; + w: number; + h: number; + }; + thumbnail: Blob; +} + +interface IAbortablePromise extends Promise { + abort(): void; +} + /** * Create a thumbnail for a image DOM element. * The image will be smaller than MAX_WIDTH and MAX_HEIGHT. @@ -51,13 +98,13 @@ export class UploadCanceledError extends Error {} * about the original image and the thumbnail. * * @param {HTMLElement} element The element to thumbnail. - * @param {integer} inputWidth The width of the image in the input element. - * @param {integer} inputHeight the width of the image in the input element. + * @param {number} inputWidth The width of the image in the input element. + * @param {number} inputHeight the width of the image in the input element. * @param {String} mimeType The mimeType to save the blob as. * @return {Promise} A promise that resolves with an object with an info key * and a thumbnail key. */ -function createThumbnail(element, inputWidth, inputHeight, mimeType) { +function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise { return new Promise((resolve) => { let targetWidth = inputWidth; let targetHeight = inputHeight; @@ -98,7 +145,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) { * @param {File} imageFile The file to load in an image element. * @return {Promise} A promise that resolves with the html image element. */ -async function loadImageElement(imageFile) { +async function loadImageElement(imageFile: File) { // Load the file into an html element const img = document.createElement("img"); const objectUrl = URL.createObjectURL(imageFile); @@ -128,8 +175,7 @@ async function loadImageElement(imageFile) { for (const chunk of chunks) { if (chunk.name === 'pHYs') { if (chunk.data.byteLength !== PHYS_HIDPI.length) return; - const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]); - return hidpi; + return chunk.data.every((val, i) => val === PHYS_HIDPI[i]); } } return false; @@ -152,7 +198,7 @@ async function loadImageElement(imageFile) { */ function infoForImageFile(matrixClient, roomId, imageFile) { let thumbnailType = "image/png"; - if (imageFile.type == "image/jpeg") { + if (imageFile.type === "image/jpeg") { thumbnailType = "image/jpeg"; } @@ -175,15 +221,15 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @param {File} videoFile The file to load in an video element. * @return {Promise} A promise that resolves with the video image element. */ -function loadVideoElement(videoFile) { +function loadVideoElement(videoFile): Promise { return new Promise((resolve, reject) => { // Load the file into an html element const video = document.createElement("video"); const reader = new FileReader(); - reader.onload = function(e) { - video.src = e.target.result; + reader.onload = function(ev) { + video.src = ev.target.result as string; // Once ready, returns its size // Wait until we have enough data to thumbnail the first frame. @@ -231,11 +277,11 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * @return {Promise} A promise that resolves with an ArrayBuffer when the file * is read. */ -function readFileAsArrayBuffer(file) { +function readFileAsArrayBuffer(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { - resolve(e.target.result); + resolve(e.target.result as ArrayBuffer); }; reader.onerror = function(e) { reject(e); @@ -257,11 +303,11 @@ function readFileAsArrayBuffer(file) { * If the file is unencrypted then the object will have a "url" key. * If the file is encrypted then the object will have a "file" key. */ -function uploadFile(matrixClient, roomId, file, progressHandler) { +function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { + let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. // First read the file into memory. - let canceled = false; let uploadPromise; let encryptInfo; const prom = readFileAsArrayBuffer(file).then(function(data) { @@ -278,9 +324,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { progressHandler: progressHandler, includeFilename: false, }); - return uploadPromise; }).then(function(url) { + if (canceled) throw new UploadCanceledError(); // If the attachment is encrypted then bundle the URL along // with the information needed to decrypt the attachment and // add it under a file key. @@ -290,7 +336,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { } return {"file": encryptInfo}; }); - prom.abort = () => { + (prom as IAbortablePromise).abort = () => { canceled = true; if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); }; @@ -300,55 +346,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { progressHandler: progressHandler, }); const promise1 = basePromise.then(function(url) { + if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. return {"url": url}; }); - // XXX: copy over the abort method to the new promise - promise1.abort = basePromise.abort; + promise1.abort = () => { + canceled = true; + MatrixClientPeg.get().cancelUpload(basePromise); + }; return promise1; } } export default class ContentMessages { - constructor() { - this.inprogress = []; - this.nextId = 0; - this._mediaConfig = null; - } + private inprogress: IUpload[] = []; + private mediaConfig: IMediaConfig = null; - static sharedInstance() { - if (global.mx_ContentMessages === undefined) { - global.mx_ContentMessages = new ContentMessages(); - } - return global.mx_ContentMessages; - } - - _isFileSizeAcceptable(file) { - if (this._mediaConfig !== null && - this._mediaConfig["m.upload.size"] !== undefined && - file.size > this._mediaConfig["m.upload.size"]) { - return false; - } - return true; - } - - _ensureMediaConfigFetched() { - if (this._mediaConfig !== null) return; - - console.log("[Media Config] Fetching"); - return MatrixClientPeg.get().getMediaConfig().then((config) => { - console.log("[Media Config] Fetched config:", config); - return config; - }).catch(() => { - // Media repo can't or won't report limits, so provide an empty object (no limits). - console.log("[Media Config] Could not fetch config, so not limiting uploads."); - return {}; - }).then((config) => { - this._mediaConfig = config; - }); - } - - sendStickerContentToRoom(url, roomId, info, text, matrixClient) { + sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); throw e; @@ -356,14 +370,14 @@ export default class ContentMessages { } getUploadLimit() { - if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) { - return this._mediaConfig["m.upload.size"]; + if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) { + return this.mediaConfig["m.upload.size"]; } else { return null; } } - async sendContentListToRoom(files, roomId, matrixClient) { + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) { if (matrixClient.isGuest()) { dis.dispatch({action: 'require_registration'}); return; @@ -372,32 +386,32 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const shouldUpload = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { - title: _t('Replying With Files'), - description: ( -
{_t( - 'At this time it is not possible to reply with a file. ' + - 'Would you like to upload this file without replying?', - )}
- ), - hasCancelButton: true, - button: _t("Continue"), - onFinished: (shouldUpload) => { - resolve(shouldUpload); - }, - }); + const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { + title: _t('Replying With Files'), + description: ( +
{_t( + 'At this time it is not possible to reply with a file. ' + + 'Would you like to upload this file without replying?', + )}
+ ), + hasCancelButton: true, + button: _t("Continue"), }); + const [shouldUpload]: [boolean] = await finished; if (!shouldUpload) return; } - await this._ensureMediaConfigFetched(); + if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to + const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); + await this.ensureMediaConfigFetched(); + modal.close(); + } const tooBigFiles = []; const okFiles = []; for (let i = 0; i < files.length; ++i) { - if (this._isFileSizeAcceptable(files[i])) { + if (this.isFileSizeAcceptable(files[i])) { okFiles.push(files[i]); } else { tooBigFiles.push(files[i]); @@ -406,17 +420,12 @@ export default class ContentMessages { if (tooBigFiles.length > 0) { const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); - const uploadFailureDialogPromise = new Promise((resolve) => { - Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { - badFiles: tooBigFiles, - totalFiles: files.length, - contentMessages: this, - onFinished: (shouldContinue) => { - resolve(shouldContinue); - }, - }); + const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { + badFiles: tooBigFiles, + totalFiles: files.length, + contentMessages: this, }); - const shouldContinue = await uploadFailureDialogPromise; + const [shouldContinue]: [boolean] = await finished; if (!shouldContinue) return; } @@ -428,31 +437,47 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { - const shouldContinue = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - onFinished: (shouldContinue, shouldUploadAll) => { - if (shouldUploadAll) { - uploadAll = true; - } - resolve(shouldContinue); - }, - }); + const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { + file, + currentIndex: i, + totalFiles: okFiles.length, }); + const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished; if (!shouldContinue) break; + if (shouldUploadAll) { + uploadAll = true; + } } - promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore); + promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore); } } - _sendContentToRoom(file, roomId, matrixClient, promBefore) { - const content = { + getCurrentUploads() { + return this.inprogress.filter(u => !u.canceled); + } + + cancelUpload(promise: Promise) { + let upload: IUpload; + for (let i = 0; i < this.inprogress.length; ++i) { + if (this.inprogress[i].promise === promise) { + upload = this.inprogress[i]; + break; + } + } + if (upload) { + upload.canceled = true; + MatrixClientPeg.get().cancelUpload(upload.promise); + dis.dispatch({action: 'upload_canceled', upload}); + } + } + + private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise) { + const content: IContent = { body: file.name || 'Attachment', info: { size: file.size, }, + msgtype: "", // set later }; // if we have a mime type for the file, add it to the message metadata @@ -461,25 +486,25 @@ export default class ContentMessages { } const prom = new Promise((resolve) => { - if (file.type.indexOf('image/') == 0) { + if (file.type.indexOf('image/') === 0) { content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { extend(content.info, imageInfo); resolve(); - }, (error)=>{ - console.error(error); + }, (e) => { + console.error(e); content.msgtype = 'm.file'; resolve(); }); - } else if (file.type.indexOf('audio/') == 0) { + } else if (file.type.indexOf('audio/') === 0) { content.msgtype = 'm.audio'; resolve(); - } else if (file.type.indexOf('video/') == 0) { + } else if (file.type.indexOf('video/') === 0) { content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { extend(content.info, videoInfo); resolve(); - }, (error)=>{ + }, (e) => { content.msgtype = 'm.file'; resolve(); }); @@ -489,19 +514,23 @@ export default class ContentMessages { } }); - const upload = { + // create temporary abort handler for before the actual upload gets passed off to js-sdk + (prom as IAbortablePromise).abort = () => { + upload.canceled = true; + }; + + const upload: IUpload = { fileName: file.name || 'Attachment', roomId: roomId, - total: 0, + total: file.size, loaded: 0, + promise: prom, }; this.inprogress.push(upload); dis.dispatch({action: 'upload_started'}); // Focus the composer view - dis.dispatch({action: 'focus_composer'}); - - let error; + dis.fire(Action.FocusComposer); function onProgress(ev) { upload.total = ev.total; @@ -509,7 +538,9 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } + let error; return prom.then(function() { + if (upload.canceled) throw new UploadCanceledError(); // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. @@ -520,16 +551,17 @@ export default class ContentMessages { content.file = result.file; content.url = result.url; }); - }).then((url) => { + }).then(() => { // Await previous message being sent into the room return promBefore; }).then(function() { + if (upload.canceled) throw new UploadCanceledError(); return matrixClient.sendMessage(roomId, content); }, function(err) { error = err; if (!upload.canceled) { let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName}); - if (err.http_status == 413) { + if (err.http_status === 413) { desc = _t( "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", {fileName: upload.fileName}, @@ -542,11 +574,9 @@ export default class ContentMessages { }); } }).finally(() => { - const inprogressKeys = Object.keys(this.inprogress); for (let i = 0; i < this.inprogress.length; ++i) { - const k = inprogressKeys[i]; - if (this.inprogress[k].promise === upload.promise) { - this.inprogress.splice(k, 1); + if (this.inprogress[i].promise === upload.promise) { + this.inprogress.splice(i, 1); break; } } @@ -555,7 +585,7 @@ export default class ContentMessages { // clear the media size limit so we fetch it again next time // we try to upload if (error && error.http_status === 413) { - this._mediaConfig = null; + this.mediaConfig = null; } dis.dispatch({action: 'upload_failed', upload, error}); } else { @@ -565,24 +595,35 @@ export default class ContentMessages { }); } - getCurrentUploads() { - return this.inprogress.filter(u => !u.canceled); + private isFileSizeAcceptable(file: File) { + if (this.mediaConfig !== null && + this.mediaConfig["m.upload.size"] !== undefined && + file.size > this.mediaConfig["m.upload.size"]) { + return false; + } + return true; } - cancelUpload(promise) { - const inprogressKeys = Object.keys(this.inprogress); - let upload; - for (let i = 0; i < this.inprogress.length; ++i) { - const k = inprogressKeys[i]; - if (this.inprogress[k].promise === promise) { - upload = this.inprogress[k]; - break; - } - } - if (upload) { - upload.canceled = true; - MatrixClientPeg.get().cancelUpload(upload.promise); - dis.dispatch({action: 'upload_canceled', upload}); + private ensureMediaConfigFetched() { + if (this.mediaConfig !== null) return; + + console.log("[Media Config] Fetching"); + return MatrixClientPeg.get().getMediaConfig().then((config) => { + console.log("[Media Config] Fetched config:", config); + return config; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + console.log("[Media Config] Could not fetch config, so not limiting uploads."); + return {}; + }).then((config) => { + this.mediaConfig = config; + }); + } + + static sharedInstance() { + if (window.mx_ContentMessages === undefined) { + window.mx_ContentMessages = new ContentMessages(); } + return window.mx_ContentMessages; } } diff --git a/src/DeviceListener.js b/src/DeviceListener.ts similarity index 51% rename from src/DeviceListener.js rename to src/DeviceListener.ts index 27caba971e..e73b56416b 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.ts @@ -14,43 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClientPeg } from './MatrixClientPeg'; -import SettingsStore from './settings/SettingsStore'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import ToastStore from './stores/ToastStore'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import { + hideToast as hideBulkUnverifiedSessionsToast, + showToast as showBulkUnverifiedSessionsToast +} from "./toasts/BulkUnverifiedSessionsToast"; +import { + hideToast as hideSetupEncryptionToast, + Kind as SetupKind, + showToast as showSetupEncryptionToast +} from "./toasts/SetupEncryptionToast"; +import { + hideToast as hideUnverifiedSessionsToast, + showToast as showUnverifiedSessionsToast +} from "./toasts/UnverifiedSessionToast"; +import {privateShouldBeEncrypted} from "./createRoom"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; -const THIS_DEVICE_TOAST_KEY = 'setupencryption'; -const OTHER_DEVICES_TOAST_KEY = 'reviewsessions'; - -function toastKey(deviceId) { - return "unverified_session_" + deviceId; -} export default class DeviceListener { + // device IDs for which the user has dismissed the verify toast ('Later') + private dismissed = new Set(); + // has the user dismissed any of the various nag toasts to setup encryption on this device? + private dismissedThisDeviceToast = false; + // cache of the key backup info + private keyBackupInfo: object = null; + private keyBackupFetchedAt: number = null; + // We keep a list of our own device IDs so we can batch ones that were already + // there the last time the app launched into a single toast, but display new + // ones in their own toasts. + private ourDeviceIdsAtStart: Set = null; + // The set of device IDs we're currently displaying toasts for + private displayingToastsForDeviceIds = new Set(); + static sharedInstance() { - if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); - return global.mx_DeviceListener; - } - - constructor() { - // device IDs for which the user has dismissed the verify toast ('Later') - this._dismissed = new Set(); - // has the user dismissed any of the various nag toasts to setup encryption on this device? - this._dismissedThisDeviceToast = false; - - // cache of the key backup info - this._keyBackupInfo = null; - this._keyBackupFetchedAt = null; - - // We keep a list of our own device IDs so we can batch ones that were already - // there the last time the app launched into a single toast, but display new - // ones in their own toasts. - this._ourDeviceIdsAtStart = null; - - // The set of device IDs we're currently displaying toasts for - this._displayingToastsForDeviceIds = new Set(); + if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener(); + return window.mx_DeviceListener; } start() { @@ -74,12 +73,12 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('sync', this._onSync); } - this._dismissed.clear(); - this._dismissedThisDeviceToast = false; - this._keyBackupInfo = null; - this._keyBackupFetchedAt = null; - this._ourDeviceIdsAtStart = null; - this._displayingToastsForDeviceIds = new Set(); + this.dismissed.clear(); + this.dismissedThisDeviceToast = false; + this.keyBackupInfo = null; + this.keyBackupFetchedAt = null; + this.ourDeviceIdsAtStart = null; + this.displayingToastsForDeviceIds = new Set(); } /** @@ -87,29 +86,29 @@ export default class DeviceListener { * * @param {String[]} deviceIds List of device IDs to dismiss notifications for */ - async dismissUnverifiedSessions(deviceIds) { + async dismissUnverifiedSessions(deviceIds: Iterable) { for (const d of deviceIds) { - this._dismissed.add(d); + this.dismissed.add(d); } this._recheck(); } dismissEncryptionSetup() { - this._dismissedThisDeviceToast = true; + this.dismissedThisDeviceToast = true; this._recheck(); } _ensureDeviceIdsAtStartPopulated() { - if (this._ourDeviceIdsAtStart === null) { + if (this.ourDeviceIdsAtStart === null) { const cli = MatrixClientPeg.get(); - this._ourDeviceIdsAtStart = new Set( + this.ourDeviceIdsAtStart = new Set( cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId), ); } } - _onWillUpdateDevices = async (users, initialFetch) => { + _onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { // If we didn't know about *any* devices before (ie. it's fresh login), // then they are all pre-existing devices, so ignore this and set the // devicesAtStart list to the devices that we see after the fetch. @@ -122,17 +121,17 @@ export default class DeviceListener { // before we download any new ones. } - _onDevicesUpdated = (users) => { + _onDevicesUpdated = (users: string[]) => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; this._recheck(); } - _onDeviceVerificationChanged = (userId) => { + _onDeviceVerificationChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; this._recheck(); } - _onUserTrustStatusChanged = (userId, trustLevel) => { + _onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; this._recheck(); } @@ -163,20 +162,25 @@ export default class DeviceListener { // & cache the result async _getKeyBackupInfo() { const now = (new Date()).getTime(); - if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { - this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - this._keyBackupFetchedAt = now; + if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { + this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + this.keyBackupFetchedAt = now; } - return this._keyBackupInfo; + return this.keyBackupInfo; + } + + private shouldShowSetupEncryptionToast() { + // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false + // then do not show the toasts until user is in at least one encrypted room. + if (privateShouldBeEncrypted()) return true; + const cli = MatrixClientPeg.get(); + return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId)); } async _recheck() { const cli = MatrixClientPeg.get(); - if ( - !SettingsStore.getValue("feature_cross_signing") || - !await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ) return; + if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; if (!cli.isCryptoEnabled()) return; // don't recheck until the initial sync is complete: lots of account data events will fire @@ -186,48 +190,25 @@ export default class DeviceListener { const crossSigningReady = await cli.isCrossSigningReady(); - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - } else { - if (!crossSigningReady) { - // make sure our keys are finished downlaoding - await cli.downloadKeys([cli.getUserId()]); - // cross signing isn't enabled - nag to enable it - // There are 3 different toasts for: - if (cli.getStoredCrossSigningForUser(cli.getUserId())) { - // Cross-signing on account but this device doesn't trust the master key (verify this session) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Verify this session"), - icon: "verification_warning", - props: {kind: 'verify_this_session'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } else { - const backupInfo = await this._getKeyBackupInfo(); - if (backupInfo) { - // No cross-signing on account but key backup available (upgrade encryption) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Encryption upgrade available"), - icon: "verification_warning", - props: {kind: 'upgrade_encryption'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } else { - // No cross-signing or key backup on account (set up encryption) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Set up encryption"), - icon: "verification_warning", - props: {kind: 'set_up_encryption'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } - } + if (this.dismissedThisDeviceToast || crossSigningReady) { + hideSetupEncryptionToast(); + } else if (this.shouldShowSetupEncryptionToast()) { + // make sure our keys are finished downloading + await cli.downloadKeys([cli.getUserId()]); + // cross signing isn't enabled - nag to enable it + // There are 3 different toasts for: + if (cli.getStoredCrossSigningForUser(cli.getUserId())) { + // Cross-signing on account but this device doesn't trust the master key (verify this session) + showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); } else { - // cross-signing is ready, and we don't need to upgrade encryption - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + const backupInfo = await this._getKeyBackupInfo(); + if (backupInfo) { + // No cross-signing on account but key backup available (upgrade encryption) + showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); + } else { + // No cross-signing or key backup on account (set up encryption) + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } } } @@ -239,20 +220,20 @@ export default class DeviceListener { // (technically could just be a boolean: we don't actually // need to remember the device IDs, but for the sake of // symmetry...). - const oldUnverifiedDeviceIds = new Set(); + const oldUnverifiedDeviceIds = new Set(); // Unverified devices that have appeared since then - const newUnverifiedDeviceIds = new Set(); + const newUnverifiedDeviceIds = new Set(); // as long as cross-signing isn't ready, // you can't see or dismiss any device toasts if (crossSigningReady) { const devices = cli.getStoredDevicesForUser(cli.getUserId()); for (const device of devices) { - if (device.deviceId == cli.deviceId) continue; + if (device.deviceId === cli.deviceId) continue; const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); - if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) { - if (this._ourDeviceIdsAtStart.has(device.deviceId)) { + if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { + if (this.ourDeviceIdsAtStart.has(device.deviceId)) { oldUnverifiedDeviceIds.add(device.deviceId); } else { newUnverifiedDeviceIds.add(device.deviceId); @@ -263,38 +244,23 @@ export default class DeviceListener { // Display or hide the batch toast for old unverified sessions if (oldUnverifiedDeviceIds.size > 0) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: OTHER_DEVICES_TOAST_KEY, - title: _t("Review where you’re logged in"), - icon: "verification_warning", - priority: ToastStore.PRIORITY_LOW, - props: { - deviceIds: oldUnverifiedDeviceIds, - }, - component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"), - }); + showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); } else { - ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); + hideBulkUnverifiedSessionsToast(); } // Show toasts for new unverified devices if they aren't already there for (const deviceId of newUnverifiedDeviceIds) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: toastKey(deviceId), - title: _t("New login. Was this you?"), - icon: "verification_warning", - props: { deviceId }, - component: sdk.getComponent("toasts.UnverifiedSessionToast"), - }); + showUnverifiedSessionsToast(deviceId); } // ...and hide any we don't need any more - for (const deviceId of this._displayingToastsForDeviceIds) { + for (const deviceId of this.displayingToastsForDeviceIds) { if (!newUnverifiedDeviceIds.has(deviceId)) { - ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); + hideUnverifiedSessionsToast(deviceId); } } - this._displayingToastsForDeviceIds = newUnverifiedDeviceIds; + this.displayingToastsForDeviceIds = newUnverifiedDeviceIds; } } diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 9131a89e5d..e7ae3217bb 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -22,6 +22,7 @@ import { _t } from './languageHandler'; import {MatrixClientPeg} from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; import {allSettled} from "./utils/promise"; +import StyledCheckbox from './components/views/elements/StyledCheckbox'; export function showGroupInviteDialog(groupId) { return new Promise((resolve, reject) => { @@ -61,19 +62,19 @@ export function showGroupAddRoomDialog(groupId) {
{ _t("Which rooms would you like to add to this community?") }
; - const checkboxContainer = ; + const checkboxContainer = + { _t("Show these rooms to non-members on the community page and room list?") } + ; const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { title: _t("Add rooms to the community"), description: description, extraNode: checkboxContainer, - placeholder: _t("Room name or alias"), + placeholder: _t("Room name or address"), button: _t("Add to community"), pickerType: 'room', validAddressTypes: ['mx-room-id'], diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js deleted file mode 100644 index ceaff0c54d..0000000000 --- a/src/KeyRequestHandler.js +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2020 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 * as sdk from './index'; -import Modal from './Modal'; -import SettingsStore from './settings/SettingsStore'; - -// TODO: We can remove this once cross-signing is the only way. -// https://github.com/vector-im/riot-web/issues/11908 -export default class KeyRequestHandler { - constructor(matrixClient) { - this._matrixClient = matrixClient; - - // the user/device for which we currently have a dialog open - this._currentUser = null; - this._currentDevice = null; - - // userId -> deviceId -> [keyRequest] - this._pendingKeyRequests = Object.create(null); - } - - handleKeyRequest(keyRequest) { - // Ignore own device key requests if cross-signing lab enabled - if (SettingsStore.getValue("feature_cross_signing")) { - return; - } - - const userId = keyRequest.userId; - const deviceId = keyRequest.deviceId; - const requestId = keyRequest.requestId; - - if (!this._pendingKeyRequests[userId]) { - this._pendingKeyRequests[userId] = Object.create(null); - } - if (!this._pendingKeyRequests[userId][deviceId]) { - this._pendingKeyRequests[userId][deviceId] = []; - } - - // check if we already have this request - const requests = this._pendingKeyRequests[userId][deviceId]; - if (requests.find((r) => r.requestId === requestId)) { - console.log("Already have this key request, ignoring"); - return; - } - - requests.push(keyRequest); - - if (this._currentUser) { - // ignore for now - console.log("Key request, but we already have a dialog open"); - return; - } - - this._processNextRequest(); - } - - handleKeyRequestCancellation(cancellation) { - // Ignore own device key requests if cross-signing lab enabled - if (SettingsStore.getValue("feature_cross_signing")) { - return; - } - - // see if we can find the request in the queue - const userId = cancellation.userId; - const deviceId = cancellation.deviceId; - const requestId = cancellation.requestId; - - if (userId === this._currentUser && deviceId === this._currentDevice) { - console.log( - "room key request cancellation for the user we currently have a" - + " dialog open for", - ); - // TODO: update the dialog. For now, we just ignore the - // cancellation. - return; - } - - if (!this._pendingKeyRequests[userId]) { - return; - } - const requests = this._pendingKeyRequests[userId][deviceId]; - if (!requests) { - return; - } - const idx = requests.findIndex((r) => r.requestId === requestId); - if (idx < 0) { - return; - } - console.log("Forgetting room key request"); - requests.splice(idx, 1); - if (requests.length === 0) { - delete this._pendingKeyRequests[userId][deviceId]; - if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { - delete this._pendingKeyRequests[userId]; - } - } - } - - _processNextRequest() { - const userId = Object.keys(this._pendingKeyRequests)[0]; - if (!userId) { - return; - } - const deviceId = Object.keys(this._pendingKeyRequests[userId])[0]; - if (!deviceId) { - return; - } - console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`); - - const finished = (r) => { - this._currentUser = null; - this._currentDevice = null; - - if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) { - // request was removed in the time the dialog was displayed - this._processNextRequest(); - return; - } - - if (r) { - for (const req of this._pendingKeyRequests[userId][deviceId]) { - req.share(); - } - } - delete this._pendingKeyRequests[userId][deviceId]; - if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { - delete this._pendingKeyRequests[userId]; - } - - this._processNextRequest(); - }; - - const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog"); - Modal.appendTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, { - matrixClient: this._matrixClient, - userId: userId, - deviceId: deviceId, - onFinished: finished, - }); - this._currentUser = userId; - this._currentDevice = deviceId; - } -} - diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 22c5d48317..96cefaf593 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -41,6 +41,7 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; +import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -163,14 +164,16 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - if (!queryParams.homeserver) { + const homeserver = localStorage.getItem(HOMESERVER_URL_KEY); + const identityServer = localStorage.getItem(ID_SERVER_URL_KEY); + if (!homeserver) { console.warn("Cannot log in with token: can't determine HS URL to use"); return Promise.resolve(false); } return sendLoginRequest( - queryParams.homeserver, - queryParams.identityServer, + homeserver, + identityServer, "m.login.token", { token: queryParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, @@ -256,8 +259,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { * @returns {Object} Information about the session - see implementation for variables. */ export function getLocalStorageSessionVars() { - const hsUrl = localStorage.getItem("mx_hs_url"); - const isUrl = localStorage.getItem("mx_is_url"); + const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); const accessToken = localStorage.getItem("mx_access_token"); const userId = localStorage.getItem("mx_user_id"); const deviceId = localStorage.getItem("mx_device_id"); @@ -298,6 +301,8 @@ async function _restoreFromLocalStorage(opts) { return false; } + const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); + console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ userId: userId, @@ -306,6 +311,7 @@ async function _restoreFromLocalStorage(opts) { homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: isGuest, + pickleKey: pickleKey, }, false); return true; } else { @@ -348,9 +354,13 @@ async function _handleLoadSessionFailure(e) { * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ -export function setLoggedIn(credentials) { +export async function setLoggedIn(credentials) { stopMatrixClient(); - return _doSetLoggedIn(credentials, true); + const pickleKey = credentials.userId && credentials.deviceId + ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) + : null; + + return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true); } /** @@ -479,9 +489,9 @@ function _showStorageEvictedDialog() { class AbortLoginAndRebuildStorage extends Error { } function _persistCredentialsToLocalStorage(credentials) { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); + localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); if (credentials.identityServerUrl) { - localStorage.setItem("mx_is_url", credentials.identityServerUrl); + localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl); } localStorage.setItem("mx_user_id", credentials.userId); localStorage.setItem("mx_access_token", credentials.accessToken); @@ -516,7 +526,9 @@ export function logout() { } _isLoggingOut = true; - MatrixClientPeg.get().logout().then(onLoggedOut, + const client = MatrixClientPeg.get(); + PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId()); + client.logout().then(onLoggedOut, (err) => { // Just throwing an error here is going to be very unhelpful // if you're trying to log out because your server's down and @@ -575,10 +587,12 @@ async function startMatrixClient(startSyncing=true) { // to work). dis.dispatch({action: 'will_start_client'}, true); + // reset things first just in case + TypingStore.sharedInstance().reset(); + ToastStore.sharedInstance().reset(); + Notifier.start(); UserActivity.sharedInstance().start(); - TypingStore.sharedInstance().reset(); // just in case - ToastStore.sharedInstance().reset(); DMRoomMap.makeShared().start(); IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); @@ -608,7 +622,7 @@ async function startMatrixClient(startSyncing=true) { } // Now that we have a MatrixClientPeg, update the Jitsi info - await Jitsi.getInstance().update(); + await Jitsi.getInstance().start(); // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.ts similarity index 74% rename from src/MatrixClientPeg.js rename to src/MatrixClientPeg.ts index 21f05b9759..bc550c1935 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.ts @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClient, MemoryStore} from 'matrix-js-sdk'; - +import {MatrixClient} from 'matrix-js-sdk/src/client'; +import {MemoryStore} from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set'; @@ -34,37 +34,26 @@ import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; -interface MatrixClientCreds { +export interface IMatrixClientCreds { homeserverUrl: string, identityServerUrl: string, userId: string, deviceId: string, accessToken: string, guest: boolean, + pickleKey?: string, } -/** - * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk - * Handles the creation/initialisation of client objects. - * This module provides a singleton instance of this class so the 'current' - * Matrix Client object is available easily. - */ -class _MatrixClientPeg { - constructor() { - this.matrixClient = null; - this._justRegisteredUserId = null; +// TODO: Move this to the js-sdk +export interface IOpts { + initialSyncLimit?: number; + pendingEventOrdering?: "detached" | "chronological"; + lazyLoadMembers?: boolean; + clientWellKnownPollPeriod?: number; +} - // These are the default options used when when the - // client is started in 'start'. These can be altered - // at any time up to after the 'will_start_client' - // event is finished processing. - this.opts = { - initialSyncLimit: 20, - }; - // the credentials used to init the current client object. - // used if we tear it down & recreate it with a different store - this._currentClientCreds = null; - } +export interface IMatrixClientPeg { + opts: IOpts; /** * Sets the script href passed to the IndexedDB web worker @@ -73,19 +62,23 @@ class _MatrixClientPeg { * * @param {string} script href to the script to be passed to the web worker */ - setIndexedDbWorkerScript(script) { - createMatrixClient.indexedDbWorkerScript = script; - } + setIndexedDbWorkerScript(script: string): void; - get(): MatrixClient { - return this.matrixClient; - } + /** + * Return the server name of the user's homeserver + * Throws an error if unable to deduce the homeserver name + * (eg. if the user is not logged in) + * + * @returns {string} The homeserver name, if present. + */ + getHomeserverName(): string; - unset() { - this.matrixClient = null; + get(): MatrixClient; + unset(): void; + assign(): Promise; + start(): Promise; - MatrixActionCreators.stop(); - } + getCredentials(): IMatrixClientCreds; /** * If we've registered a user ID we set this to the ID of the @@ -95,9 +88,7 @@ class _MatrixClientPeg { * * @param {string} uid The user ID of the user we've just registered */ - setJustRegisteredUserId(uid) { - this._justRegisteredUserId = uid; - } + setJustRegisteredUserId(uid: string): void; /** * Returns true if the current user has just been registered by this @@ -105,23 +96,73 @@ class _MatrixClientPeg { * * @returns {bool} True if user has just been registered */ - currentUserIsJustRegistered() { + currentUserIsJustRegistered(): boolean; + + /** + * Replace this MatrixClientPeg's client with a client instance that has + * homeserver / identity server URLs and active credentials + * + * @param {IMatrixClientCreds} creds The new credentials to use. + */ + replaceUsingCreds(creds: IMatrixClientCreds): void; +} + +/** + * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk + * Handles the creation/initialisation of client objects. + * This module provides a singleton instance of this class so the 'current' + * Matrix Client object is available easily. + */ +class _MatrixClientPeg implements IMatrixClientPeg { + // These are the default options used when when the + // client is started in 'start'. These can be altered + // at any time up to after the 'will_start_client' + // event is finished processing. + public opts: IOpts = { + initialSyncLimit: 20, + }; + + private matrixClient: MatrixClient = null; + private justRegisteredUserId: string; + + // the credentials used to init the current client object. + // used if we tear it down & recreate it with a different store + private currentClientCreds: IMatrixClientCreds; + + constructor() { + } + + public setIndexedDbWorkerScript(script: string): void { + createMatrixClient.indexedDbWorkerScript = script; + } + + public get(): MatrixClient { + return this.matrixClient; + } + + public unset(): void { + this.matrixClient = null; + + MatrixActionCreators.stop(); + } + + public setJustRegisteredUserId(uid: string): void { + this.justRegisteredUserId = uid; + } + + public currentUserIsJustRegistered(): boolean { return ( this.matrixClient && - this.matrixClient.credentials.userId === this._justRegisteredUserId + this.matrixClient.credentials.userId === this.justRegisteredUserId ); } - /* - * Replace this MatrixClientPeg's client with a client instance that has - * homeserver / identity server URLs and active credentials - */ - replaceUsingCreds(creds: MatrixClientCreds) { - this._currentClientCreds = creds; - this._createClient(creds); + public replaceUsingCreds(creds: IMatrixClientCreds): void { + this.currentClientCreds = creds; + this.createClient(creds); } - async assign() { + public async assign(): Promise { for (const dbType of ['indexeddb', 'memory']) { try { const promise = this.matrixClient.store.startup(); @@ -132,7 +173,7 @@ class _MatrixClientPeg { if (dbType === 'indexeddb') { console.error('Error starting matrixclient store - falling back to memory store', err); this.matrixClient.store = new MemoryStore({ - localStorage: global.localStorage, + localStorage: localStorage, }); } else { console.error('Failed to start memory store!', err); @@ -158,9 +199,7 @@ class _MatrixClientPeg { // The js-sdk found a crypto DB too new for it to use const CryptoStoreTooNewDialog = sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); - Modal.createDialog(CryptoStoreTooNewDialog, { - host: window.location.host, - }); + Modal.createDialog(CryptoStoreTooNewDialog); } // this can happen for a number of reasons, the most likely being // that the olm library was missing. It's not fatal. @@ -171,6 +210,7 @@ class _MatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; opts.lazyLoadMembers = true; + opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours // Connect the matrix client to the dispatcher and setting handlers MatrixActionCreators.start(this.matrixClient); @@ -179,7 +219,7 @@ class _MatrixClientPeg { return opts; } - async start() { + public async start(): Promise { const opts = await this.assign(); console.log(`MatrixClientPeg: really starting MatrixClient`); @@ -187,7 +227,7 @@ class _MatrixClientPeg { console.log(`MatrixClientPeg: MatrixClient started`); } - getCredentials(): MatrixClientCreds { + public getCredentials(): IMatrixClientCreds { return { homeserverUrl: this.matrixClient.baseUrl, identityServerUrl: this.matrixClient.idBaseUrl, @@ -198,12 +238,7 @@ class _MatrixClientPeg { }; } - /* - * Return the server name of the user's homeserver - * Throws an error if unable to deduce the homeserver name - * (eg. if the user is not logged in) - */ - getHomeserverName() { + public getHomeserverName(): string { const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); if (matches === null || matches.length < 1) { throw new Error("Failed to derive homeserver name from user ID!"); @@ -211,13 +246,15 @@ class _MatrixClientPeg { return matches[1]; } - _createClient(creds: MatrixClientCreds) { + private createClient(creds: IMatrixClientCreds): void { + // TODO: Make these opts typesafe with the js-sdk const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, userId: creds.userId, deviceId: creds.deviceId, + pickleKey: creds.pickleKey, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), @@ -228,9 +265,9 @@ class _MatrixClientPeg { ], unstableClientRelationAggregation: true, identityServer: new IdentityAuthClient(), + cryptoCallbacks: {}, }; - opts.cryptoCallbacks = {}; // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. @@ -253,8 +290,8 @@ class _MatrixClientPeg { } } -if (!global.mxMatrixClientPeg) { - global.mxMatrixClientPeg = new _MatrixClientPeg(); +if (!window.mxMatrixClientPeg) { + window.mxMatrixClientPeg = new _MatrixClientPeg(); } -export const MatrixClientPeg = global.mxMatrixClientPeg; +export const MatrixClientPeg = window.mxMatrixClientPeg; diff --git a/src/Notifier.js b/src/Notifier.js index 2ffa92452b..cd328ba565 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -26,6 +26,9 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import { + hideToast as hideNotificationsToast, +} from "./toasts/DesktopNotificationsToast"; /* * Dispatches: @@ -278,12 +281,7 @@ const Notifier = { Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); - // XXX: why are we dispatching this here? - // this is nothing to do with notifier_enabled - dis.dispatch({ - action: "notifier_enabled", - value: this.isEnabled(), - }); + hideNotificationsToast(); // update the info to localStorage for persistent settings if (persistent && global.localStorage) { diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 320599f6d9..9472ddc633 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -84,8 +84,14 @@ export default class PasswordReset { try { await this.client.setPassword({ + // Note: Though this sounds like a login type for identity servers only, it + // has a dual purpose of being used for homeservers too. type: "m.login.email.identity", + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + // See https://github.com/matrix-org/matrix-doc/issues/2220 threepid_creds: creds, + threepidCreds: creds, }, this.password); } catch (err) { if (err.httpStatus === 401) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index d81da80e8d..15798ae3b1 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -450,8 +450,8 @@ export const Commands = [ new Command({ command: 'join', aliases: ['j', 'goto'], - args: '', - description: _td('Joins room with given alias'), + args: '', + description: _td('Joins room with given address'), runFn: function(_, args) { if (args) { // Note: we support 2 versions of this command. The first is @@ -562,7 +562,7 @@ export const Commands = [ }), new Command({ command: 'part', - args: '[]', + args: '[]', description: _td('Leave room'), runFn: function(roomId, args) { const cli = MatrixClientPeg.get(); @@ -594,7 +594,7 @@ export const Commands = [ } if (targetRoomId) break; } - if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias); + if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias); } } diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js deleted file mode 100644 index 9eb4439816..0000000000 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ /dev/null @@ -1,206 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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 createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {Key} from "../../../Keyboard"; -import * as sdk from "../../../index"; - -// XXX: This component is not cross-signing aware. -// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this -// component or taking it out to pasture. -export default createReactClass({ - displayName: 'EncryptedEventDialog', - - propTypes: { - event: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }, - - getInitialState: function() { - return { device: null }; - }, - - componentDidMount: function() { - this._unmounted = false; - const client = MatrixClientPeg.get(); - - // first try to load the device from our store. - // - this.refreshDevice().then((dev) => { - if (dev) { - return dev; - } - - // tell the client to try to refresh the device list for this user - return client.downloadKeys([this.props.event.getSender()], true).then(() => { - return this.refreshDevice(); - }); - }).then((dev) => { - if (this._unmounted) { - return; - } - - this.setState({ device: dev }); - client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); - }, (err)=>{ - console.log("Error downloading devices", err); - }); - }, - - componentWillUnmount: function() { - this._unmounted = true; - const client = MatrixClientPeg.get(); - if (client) { - client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - } - }, - - refreshDevice: function() { - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event)); - }, - - onDeviceVerificationChanged: function(userId, device) { - if (userId === this.props.event.getSender()) { - this.refreshDevice().then((dev) => { - this.setState({ device: dev }); - }); - } - }, - - onKeyDown: function(e) { - if (e.key === Key.ESCAPE) { - e.stopPropagation(); - e.preventDefault(); - this.props.onFinished(false); - } - }, - - _renderDeviceInfo: function() { - const device = this.state.device; - if (!device) { - return ({ _t('unknown device') }); - } - - let verificationStatus = ({ _t('NOT verified') }); - if (device.isBlocked()) { - verificationStatus = ({ _t('Blacklisted') }); - } else if (device.isVerified()) { - verificationStatus = _t('verified'); - } - - return ( - - - - - - - - - - - - - - - - - - - -
{ _t('Name') }{ device.getDisplayName() }
{ _t('Device ID') }{ device.deviceId }
{ _t('Verification') }{ verificationStatus }
{ _t('Ed25519 fingerprint') }{ device.getFingerprint() }
- ); - }, - - _renderEventInfo: function() { - const event = this.props.event; - - return ( - - - - - - - - - - - - - - - - - - - { - event.getContent().msgtype === 'm.bad.encrypted' ? ( - - - - - ) : null - } - - - - - -
{ _t('User ID') }{ event.getSender() }
{ _t('Curve25519 identity key') }{ event.getSenderKey() || { _t('none') } }
{ _t('Claimed Ed25519 fingerprint key') }{ event.getKeysClaimed().ed25519 || { _t('none') } }
{ _t('Algorithm') }{ event.getWireContent().algorithm || { _t('unencrypted') } }
{ _t('Decryption error') }{ event.getContent().body }
{ _t('Session ID') }{ event.getWireContent().session_id || { _t('none') } }
- ); - }, - - render: function() { - const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); - - let buttons = null; - if (this.state.device) { - buttons = ( - - ); - } - - return ( -
-
- { _t('End-to-end encryption information') } -
-
-

{ _t('Event information') }

- { this._renderEventInfo() } - -

{ _t('Sender session information') }

- { this._renderDeviceInfo() } -
-
- - { buttons } -
-
- ); - }, -}); diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 532b2f960f..79fbb98c7b 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import PropTypes from 'prop-types'; import {_t, _td} from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; -import SettingsStore from '../../../../settings/SettingsStore'; import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import {copyNode} from "../../../../utils/strings"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; @@ -67,10 +66,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { async componentDidMount() { const cli = MatrixClientPeg.get(); - const secureSecretStorage = ( - SettingsStore.getValue("feature_cross_signing") && - await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ); + const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); this.setState({ secureSecretStorage }); // If we're using secret storage, skip ahead to the backing up step, as @@ -284,8 +280,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let matchText; + let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. // Security concious readers will note that if you left riot-web unattended @@ -295,6 +293,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { // Note that not having typed anything at all will not hit this clause and // fall through so empty box === no hint. matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } let passPhraseMatch = null; @@ -303,7 +302,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{matchText}
- {_t("Go back to set it again.")} + {changeText}
; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 12b71206d0..d7b79c2cfa 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type: 'm.id.user', user: MatrixClientPeg.get().getUserId(), }, - // https://github.com/matrix-org/synapse/issues/5665 + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 user: MatrixClientPeg.get().getUserId(), password: this.state.accountPassword, }); @@ -538,8 +539,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const Field = sdk.getComponent('views.elements.Field'); let matchText; + let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. // Security concious readers will note that if you left riot-web unattended @@ -549,6 +552,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // Note that not having typed anything at all will not hit this clause and // fall through so empty box === no hint. matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } let passPhraseMatch = null; @@ -557,7 +561,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
{matchText}
- {_t("Go back to set it again.")} + {changeText}
; diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index 3edb1ff81d..d7eac59f91 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -90,11 +90,12 @@ export default class CommunityProvider extends AutocompleteProvider { type: "community", href: makeGroupPermalink(groupId), component: ( - - } title={name} description={groupId} /> + ), range, })) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 19a7a969d6..0ee0088f02 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {forwardRef} from 'react'; import classNames from 'classnames'; /* These were earlier stateless functional components but had to be converted @@ -30,50 +30,37 @@ interface ITextualCompletionProps { className?: string; } -export class TextualCompletion extends React.PureComponent { - render() { - const { - title, - subtitle, - description, - className, - ...restProps - } = this.props; - return ( -
- { title } - { subtitle } - { description } -
- ); - } +export const TextualCompletion = forwardRef((props, ref) => { + const {title, subtitle, description, className, ...restProps} = props; + return ( +
+ { title } + { subtitle } + { description } +
+ ); +}); + +interface IPillCompletionProps extends ITextualCompletionProps { + children?: React.ReactNode, } -interface IPillCompletionProps { - title?: string; - subtitle?: string; - description?: string; - initialComponent?: React.ReactNode, - className?: string; -} - -export class PillCompletion extends React.PureComponent { - render() { - const { - title, - subtitle, - description, - initialComponent, - className, - ...restProps - } = this.props; - return ( -
- { initialComponent } - { title } - { subtitle } - { description } -
- ); - } -} +export const PillCompletion = forwardRef((props, ref) => { + const {title, subtitle, description, className, children, ...restProps} = props; + return ( +
+ { children } + { title } + { subtitle } + { description } +
+ ); +}); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b67e26117b..c4dd3ec9cc 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { + this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { keys: ['emoji.emoticon', 'shortname'], funcs: [ (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases @@ -121,9 +121,9 @@ export default class EmojiProvider extends AutocompleteProvider { return { completion: unicode, component: ( - { unicode } - } /> + + { unicode } + ), range, }; diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index b217612b0e..ef1823c0ca 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -48,7 +48,9 @@ export default class NotifProvider extends AutocompleteProvider { type: "at-room", suffix: ' ', component: ( - } title="@room" description={_t("Notify the whole room")} /> + + + ), range, }]; diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 95bcfb25ef..2c1899d813 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -45,7 +45,7 @@ interface IOptions { * @param {function[]} options.funcs List of functions that when called with the * object as an arg will return a string to use as an index */ -export default class QueryMatcher { +export default class QueryMatcher { private _options: IOptions; private _keys: IOptions["keys"]; private _funcs: Required["funcs"]>; @@ -75,7 +75,11 @@ export default class QueryMatcher { this._items = new Map(); for (const object of objects) { - const keyValues = _at(object, this._keys); + // Need to use unsafe coerce here because the objects can have any + // type for their values. We assume that those values who's keys have + // been specified will be string. Also, we cannot infer all the + // types of the keys of the objects at compile. + const keyValues = _at(object, this._keys); for (const f of this._funcs) { keyValues.push(f(object)); diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 01e770407c..0d8aac4218 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -103,7 +103,9 @@ export default class RoomProvider extends AutocompleteProvider { suffix: ' ', href: makeRoomPermalink(room.displayedAlias), component: ( - } title={room.room.name} description={room.displayedAlias} /> + + + ), range, }; diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 1680eb5d54..eeb6c7a522 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -125,10 +125,9 @@ export default class UserProvider extends AutocompleteProvider { suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', href: makeUserPermalink(user.userId), component: ( - } - title={displayName} - description={user.userId} /> + + + ), range, }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1311d6e4f6..a946d16319 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({ Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, { title: _t('Add rooms to the community summary'), description: _t("Which rooms would you like to add to this summary?"), - placeholder: _t("Room name or alias"), + placeholder: _t("Room name or address"), button: _t("Add to summary"), pickerType: 'room', validAddressTypes: ['mx-room-id'], diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a1b4f49c56..05cd97df2a 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; -import RoomList2 from "../views/rooms/RoomList2"; +import {Action} from "../../dispatcher/actions"; const LeftPanel = createReactClass({ @@ -198,7 +198,7 @@ const LeftPanel = createReactClass({ onSearchCleared: function(source) { if (source === "keyboard") { - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } this.setState({searchExpanded: false}); }, @@ -274,28 +274,15 @@ const LeftPanel = createReactClass({ breadcrumbs = (); } - let roomList = null; - if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { - roomList = ; - } else { - roomList = ; - } + const roomList = ; return (
diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx new file mode 100644 index 0000000000..c9a4948539 --- /dev/null +++ b/src/components/structures/LeftPanel2.tsx @@ -0,0 +1,154 @@ +/* +Copyright 2020 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 * as React from "react"; +import TagPanel from "./TagPanel"; +import classNames from "classnames"; +import dis from "../../dispatcher/dispatcher"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import { _t } from "../../languageHandler"; +import SearchBox from "./SearchBox"; +import RoomList2 from "../views/rooms/RoomList2"; +import TopLeftMenuButton from "./TopLeftMenuButton"; +import { Action } from "../../dispatcher/actions"; + +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + *******************************************************************/ + +interface IProps { + // TODO: Support collapsed state +} + +interface IState { + searchExpanded: boolean; + searchFilter: string; // TODO: Move search into room list? +} + +export default class LeftPanel2 extends React.Component { + // TODO: Properly support TagPanel + // TODO: Properly support searching/filtering + // TODO: Properly support breadcrumbs + // TODO: Properly support TopLeftMenu (User Settings) + // TODO: a11y + // TODO: actually make this useful in general (match design proposals) + // TODO: Fadable support (is this still needed?) + + constructor(props: IProps) { + super(props); + + this.state = { + searchExpanded: false, + searchFilter: "", + }; + } + + private onSearch = (term: string): void => { + this.setState({searchFilter: term}); + }; + + private onSearchCleared = (source: string): void => { + if (source === "keyboard") { + dis.fire(Action.FocusComposer); + } + this.setState({searchExpanded: false}); + } + + private onSearchFocus = (): void => { + this.setState({searchExpanded: true}); + }; + + private onSearchBlur = (event: FocusEvent): void => { + const target = event.target as HTMLInputElement; + if (target.value.length === 0) { + this.setState({searchExpanded: false}); + } + } + + public render(): React.ReactNode { + const tagPanel = ( +
+ +
+ ); + + const exploreButton = ( +
+ dis.dispatch({action: 'view_room_directory'})}> + {_t("Explore")} + +
+ ); + + const searchBox = ( {/*TODO*/}} + onSearch={this.onSearch} + onCleared={this.onSearchCleared} + onFocus={this.onSearchFocus} + onBlur={this.onSearchBlur} + collapsed={false}/>); // TODO: Collapsed support + + // TODO: Improve props for RoomList2 + const roomList = {/*TODO*/}} + resizeNotifier={null} + collapsed={false} + searchFilter={this.state.searchFilter} + onFocus={() => {/*TODO*/}} + onBlur={() => {/*TODO*/}} + />; + + // TODO: Breadcrumbs + // TODO: Conference handling / calls + + const containerClasses = classNames({ + "mx_LeftPanel_container": true, + "mx_fadable": true, + "collapsed": false, // TODO: Collapsed support + "mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support + "mx_fadable_faded": false, + "mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator) + }); + + return ( +
+ {tagPanel} + +
+ ); + } +} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 148d10fe8d..0504e3a76a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import sessionStore from '../../stores/SessionStore'; -import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; +import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import TagOrderActions from '../../actions/TagOrderActions'; @@ -43,6 +43,17 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; import { DefaultTagID } from "../../stores/room-list/models"; +import { + showToast as showSetPasswordToast, + hideToast as hideSetPasswordToast +} from "../../toasts/SetPasswordToast"; +import { + showToast as showServerLimitToast, + hideToast as hideServerLimitToast +} from "../../toasts/ServerLimitToast"; +import { Action } from "../../dispatcher/actions"; +import LeftPanel2 from "./LeftPanel2"; + // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -57,7 +68,7 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; - onRegistered: (credentials: MatrixClientCreds) => Promise; + onRegistered: (credentials: IMatrixClientCreds) => Promise; viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; @@ -65,10 +76,6 @@ interface IProps { initialEventPixelOffset: number; leftDisabled: boolean; rightDisabled: boolean; - showCookieBar: boolean; - hasNewVersion: boolean; - userHasGeneratedPassword: boolean; - showNotifierToolbar: boolean; page_type: string; autoJoin: boolean; thirdPartyInvite?: object; @@ -76,7 +83,6 @@ interface IProps { currentRoomId: string; ConferenceHandler?: object; collapseLhs: boolean; - checkingForUpdate: boolean; config: { piwik: { policyUrl: string; @@ -86,19 +92,26 @@ interface IProps { currentUserId?: string; currentGroupId?: string; currentGroupIsNew?: boolean; - version?: string; - newVersion?: string; - newVersionReleaseNotes?: string; } + +interface IUsageLimit { + limit_type: "monthly_active_user" | string; + admin_contact?: string; +} + interface IState { mouseDown?: { x: number; y: number; }; - syncErrorData: any; + syncErrorData?: { + error: { + data: IUsageLimit; + errcode: string; + }; + }; + usageLimitEventContent?: IUsageLimit; useCompactLayout: boolean; - serverNoticeEvents: MatrixEvent[]; - userHasGeneratedPassword: boolean; } /** @@ -141,11 +154,8 @@ class LoggedInView extends React.PureComponent { this.state = { mouseDown: undefined, syncErrorData: undefined, - userHasGeneratedPassword: false, // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), - // any currently active server notice events - serverNoticeEvents: [], }; // stash the MatrixClient in case we log out before we are unmounted @@ -179,18 +189,6 @@ class LoggedInView extends React.PureComponent { this._loadResizerPreferences(); } - componentDidUpdate(prevProps, prevState) { - // attempt to guess when a banner was opened or closed - if ( - (prevProps.showCookieBar !== this.props.showCookieBar) || - (prevProps.hasNewVersion !== this.props.hasNewVersion) || - (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) || - (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) - ) { - this.props.resizeNotifier.notifyBannersChanged(); - } - } - componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); this._matrixClient.removeListener("accountData", this.onAccountData); @@ -220,9 +218,11 @@ class LoggedInView extends React.PureComponent { }; _setStateFromSessionStore = () => { - this.setState({ - userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), - }); + if (this._sessionStore.getCachedPassword()) { + showSetPasswordToast(); + } else { + hideSetPasswordToast(); + } }; _createResizer() { @@ -294,6 +294,8 @@ class LoggedInView extends React.PureComponent { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { this._updateServerNoticeEvents(); + } else { + this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); } }; @@ -304,11 +306,24 @@ class LoggedInView extends React.PureComponent { } }; + _calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + if (error) { + usageLimitEventContent = syncErrorData.error.data; + } + + if (usageLimitEventContent) { + showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error); + } else { + hideServerLimitToast(); + } + } + _updateServerNoticeEvents = async () => { const roomLists = RoomListStoreTempProxy.getRoomLists(); if (!roomLists[DefaultTagID.ServerNotice]) return []; - const pinnedEvents = []; + const events = []; for (const room of roomLists[DefaultTagID.ServerNotice]) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); @@ -318,12 +333,19 @@ class LoggedInView extends React.PureComponent { for (const eventId of pinnedEventIds) { const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const event = timeline.getEvents().find(ev => ev.getId() === eventId); - if (event) pinnedEvents.push(event); + if (event) events.push(event); } } - this.setState({ - serverNoticeEvents: pinnedEvents, + + const usageLimitEvent = events.find((e) => { + return ( + e && e.getType() === 'm.room.message' && + e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached' + ); }); + const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); + this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); + this.setState({ usageLimitEventContent }); }; _onPaste = (ev) => { @@ -338,7 +360,7 @@ class LoggedInView extends React.PureComponent { // refocusing during a paste event will make the // paste end up in the newly focused element, // so dispatch synchronously before paste happens - dis.dispatch({action: 'focus_composer'}, true); + dis.fire(Action.FocusComposer, true); } }; @@ -488,7 +510,7 @@ class LoggedInView extends React.PureComponent { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input - dis.dispatch({action: 'focus_composer'}, true); + dis.fire(Action.FocusComposer, true); ev.stopPropagation(); // we should *not* preventDefault() here as // that would prevent typing in the now-focussed composer @@ -599,12 +621,6 @@ class LoggedInView extends React.PureComponent { const GroupView = sdk.getComponent('structures.GroupView'); const MyGroups = sdk.getComponent('structures.MyGroups'); const ToastContainer = sdk.getComponent('structures.ToastContainer'); - const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); - const CookieBar = sdk.getComponent('globals.CookieBar'); - const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); - const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar'); - const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar'); - const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar'); let pageElement; @@ -648,50 +664,25 @@ class LoggedInView extends React.PureComponent { break; } - const usageLimitEvent = this.state.serverNoticeEvents.find((e) => { - return ( - e && e.getType() === 'm.room.message' && - e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached' - ); - }); - - let topBar; - if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { - topBar = ; - } else if (usageLimitEvent) { - topBar = ; - } else if (this.props.showCookieBar && - this.props.config.piwik && - navigator.doNotTrack !== "1" - ) { - const policyUrl = this.props.config.piwik.policyUrl || null; - topBar = ; - } else if (this.props.hasNewVersion) { - topBar = ; - } else if (this.props.checkingForUpdate) { - topBar = ; - } else if (this.state.userHasGeneratedPassword) { - topBar = ; - } else if (this.props.showNotifierToolbar) { - topBar = ; - } - let bodyClasses = 'mx_MatrixChat'; - if (topBar) { - bodyClasses += ' mx_MatrixChat_toolbarShowing'; - } if (this.state.useCompactLayout) { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + let leftPanel = ( + + ); + if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { + // TODO: Supply props like collapsed and disabled to LeftPanel2 + leftPanel = ( + + ); + } + return (
{ onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp} > - { topBar }
- + { leftPanel } { pageElement }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 48dc72f4fa..69f91047b7 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes'; import { getHomePageUrl } from '../../utils/pages'; import createRoom from "../../createRoom"; -import KeyRequestHandler from '../../KeyRequestHandler'; import { _t, getCurrentLanguage } from '../../languageHandler'; import SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; import ThemeController from "../../settings/controllers/ThemeController"; @@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; -import { ThemeWatcher } from "../../theme"; -import { FontWatcher } from '../../FontWatcher'; +import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; +import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer, IDeferred } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; @@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager"; import type LoggedInViewType from "./LoggedInView"; import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../dispatcher/actions"; +import { + showToast as showAnalyticsToast, + hideToast as hideAnalyticsToast +} from "../../toasts/AnalyticsToast"; +import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; /** constants for MatrixChat.state.view */ export enum Views { @@ -169,12 +173,6 @@ interface IState { leftDisabled: boolean; middleDisabled: boolean; // the right panel's disabled state is tracked in its store. - version?: string; - newVersion?: string; - hasNewVersion: boolean; - newVersionReleaseNotes?: string; - checkingForUpdate?: string; // updateCheckStatusEnum - showCookieBar: boolean; // Parameters used in the registration dance with the IS register_client_secret?: string; register_session_id?: string; @@ -184,7 +182,6 @@ interface IState { hideToSRUsers: boolean; syncError?: Error; resizeNotifier: ResizeNotifier; - showNotifierToolbar: boolean; serverConfig?: ValidatedServerConfig; ready: boolean; thirdPartyInvite?: object; @@ -228,17 +225,10 @@ export default class MatrixChat extends React.PureComponent { leftDisabled: false, middleDisabled: false, - hasNewVersion: false, - newVersionReleaseNotes: null, - checkingForUpdate: null, - - showCookieBar: false, - hideToSRUsers: false, syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. resizeNotifier: new ResizeNotifier(), - showNotifierToolbar: false, ready: false, }; @@ -339,12 +329,6 @@ export default class MatrixChat extends React.PureComponent { }); } - if (SettingsStore.getValue("showCookieBar")) { - this.setState({ - showCookieBar: true, - }); - } - if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } @@ -363,7 +347,7 @@ export default class MatrixChat extends React.PureComponent { Analytics.trackPageChange(durationMs); } if (this.focusComposer) { - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); this.focusComposer = false; } } @@ -686,9 +670,6 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({action: 'view_my_groups'}); } break; - case 'notifier_enabled': - this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()}); - break; case 'hide_left_panel': this.setState({ collapseLhs: true, @@ -736,15 +717,6 @@ export default class MatrixChat extends React.PureComponent { case 'client_started': this.onClientStarted(); break; - case 'new_version': - this.onVersion( - payload.currentVersion, payload.newVersion, - payload.releaseNotes, - ); - break; - case 'check_updates': - this.setState({ checkingForUpdate: payload.value }); - break; case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; @@ -761,19 +733,13 @@ export default class MatrixChat extends React.PureComponent { case 'accept_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); - - this.setState({ - showCookieBar: false, - }); + hideAnalyticsToast(); Analytics.enable(); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); - - this.setState({ - showCookieBar: false, - }); + hideAnalyticsToast(); break; } }; @@ -932,9 +898,20 @@ export default class MatrixChat extends React.PureComponent { }); } - private viewGroup(payload) { + private async viewGroup(payload) { const groupId = payload.group_id; + + // Wait for the first sync to complete + if (!this.firstSyncComplete) { + if (!this.firstSyncPromise) { + console.warn('Cannot view a group before first sync. group_id:', groupId); + return; + } + await this.firstSyncPromise.promise; + } + this.setState({ + view: Views.LOGGED_IN, currentGroupId: groupId, currentGroupIsNew: payload.group_is_new, }); @@ -1251,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent { } StorageManager.tryPersistStorage(); + + if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") { + showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl); + } } private showScreenAfterLogin() { @@ -1378,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent { this.firstSyncComplete = true; this.firstSyncPromise.resolve(); - dis.dispatch({action: 'focus_composer'}); + if (Notifier.shouldShowToolbar()) { + showNotificationsToast(); + } + + dis.fire(Action.FocusComposer); this.setState({ ready: true, - showNotifierToolbar: Notifier.shouldShowToolbar(), }); }); cli.on('Call.incoming', function(call) { @@ -1460,16 +1444,6 @@ export default class MatrixChat extends React.PureComponent { cli.on("Session.logged_out", () => dft.stop()); cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); - // TODO: We can remove this once cross-signing is the only way. - // https://github.com/vector-im/riot-web/issues/11908 - const krh = new KeyRequestHandler(cli); - cli.on("crypto.roomKeyRequest", (req) => { - krh.handleKeyRequest(req); - }); - cli.on("crypto.roomKeyRequestCancellation", (req) => { - krh.handleKeyRequestCancellation(req); - }); - cli.on("Room", (room) => { if (MatrixClientPeg.get().isCryptoEnabled()) { const blacklistEnabled = SettingsStore.getValueAt( @@ -1540,13 +1514,6 @@ export default class MatrixChat extends React.PureComponent { }); cli.on("crypto.verification.request", request => { - const isFlagOn = SettingsStore.getValue("feature_cross_signing"); - - if (!isFlagOn && !request.channel.deviceId) { - request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"}); - return; - } - if (request.verifier) { const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { @@ -1559,7 +1526,7 @@ export default class MatrixChat extends React.PureComponent { icon: "verification", props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), - priority: ToastStore.PRIORITY_REALTIME, + priority: 90, }); } }); @@ -1589,9 +1556,7 @@ export default class MatrixChat extends React.PureComponent { // be aware of will be signalled through the room shield // changing colour. More advanced behaviour will come once // we implement more settings. - cli.setGlobalErrorOnUnknownDevices( - !SettingsStore.getValue("feature_cross_signing"), - ); + cli.setGlobalErrorOnUnknownDevices(false); } } @@ -1833,16 +1798,6 @@ export default class MatrixChat extends React.PureComponent { this.showScreen("settings"); }; - onVersion(current: string, latest: string, releaseNotes?: string) { - this.setState({ - version: current, - newVersion: latest, - hasNewVersion: current !== latest, - newVersionReleaseNotes: releaseNotes, - checkingForUpdate: null, - }); - } - onSendEvent(roomId: string, event: MatrixEvent) { const cli = MatrixClientPeg.get(); if (!cli) { @@ -1949,17 +1904,8 @@ export default class MatrixChat extends React.PureComponent { // whether cross-signing has been set up on the account. const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master"); if (masterKeyInStorage) { - // Auto-enable cross-signing for the new session when key found in - // secret storage. - SettingsStore.setValue("feature_cross_signing", null, SettingLevel.DEVICE, true); this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); - } else if ( - SettingsStore.getValue("feature_cross_signing") && - await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ) { - // This will only work if the feature is set to 'enable' in the config, - // since it's too early in the lifecycle for users to have turned the - // labs flag on. + } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); } else { this.onLoggedIn(); @@ -1978,7 +1924,10 @@ export default class MatrixChat extends React.PureComponent { // console.log(`Rendering MatrixChat with view ${this.state.view}`); let fragmentAfterLogin = ""; - if (this.props.initialScreenAfterLogin) { + if (this.props.initialScreenAfterLogin && + // XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop + !["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen) + ) { fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`; } @@ -2037,7 +1986,6 @@ export default class MatrixChat extends React.PureComponent { onCloseAllSettings={this.onCloseAllSettings} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} - showCookieBar={this.state.showCookieBar} /> ); } else { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 93e4668f66..d11fee6360 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -34,6 +34,30 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; +// check if there is a previous event and it has the same sender as this event +// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL +function shouldFormContinuation(prevEvent, mxEvent) { + // sanity check inputs + if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; + // check if within the max continuation period + if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; + + // Some events should appear as continuations from previous events of different types. + if (mxEvent.getType() !== prevEvent.getType() && + (!continuedTypes.includes(mxEvent.getType()) || + !continuedTypes.includes(prevEvent.getType()))) return false; + + // Check if the sender is the same and hasn't changed their displayname/avatar between these events + if (mxEvent.sender.userId !== prevEvent.sender.userId || + mxEvent.sender.name !== prevEvent.sender.name || + mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; + + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + if (!haveTileForEvent(prevEvent)) return false; + + return true; +} + const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite'; /* (almost) stateless UI component which builds the event tiles in the room timeline. @@ -108,6 +132,9 @@ export default class MessagePanel extends React.Component { // whether to show reactions for an event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }; // Force props to be loaded for useIRCLayout @@ -119,7 +146,6 @@ export default class MessagePanel extends React.Component { // display 'ghost' read markers that are animating away ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")), }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -172,8 +198,6 @@ export default class MessagePanel extends React.Component { this._showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - - this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); } componentDidMount() { @@ -183,7 +207,6 @@ export default class MessagePanel extends React.Component { componentWillUnmount() { this._isMounted = false; SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); - SettingsStore.unwatchSetting(this._layoutWatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -202,17 +225,6 @@ export default class MessagePanel extends React.Component { }); }; - onLayoutChange = () => { - this.setState({ - useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")), - }); - } - - useIRCLayout(ircLayoutSelected) { - // if room is null we are not in a normal room list - return ircLayoutSelected && this.props.room; - } - /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -527,39 +539,6 @@ export default class MessagePanel extends React.Component { const isEditing = this.props.editState && this.props.editState.getEvent().getId() === mxEv.getId(); - // is this a continuation of the previous message? - let continuation = false; - - // Some events should appear as continuations from previous events of - // different types. - - const eventTypeContinues = - prevEvent !== null && - continuedTypes.includes(mxEv.getType()) && - continuedTypes.includes(prevEvent.getType()); - - // if there is a previous event and it has the same sender as this event - // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL - if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && - // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile - haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && - (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { - continuation = true; - } - -/* - // Work out if this is still a continuation, as we are now showing commands - // and /me messages with their own little avatar. The case of a change of - // event type (commands) is handled above, but we need to handle the /me - // messages seperately as they have a msgtype of 'm.emote' but are classed - // as normal messages - if (prevEvent !== null && prevEvent.sender && mxEv.sender - && mxEv.sender.userId === prevEvent.sender.userId - && mxEv.getType() == prevEvent.getType() - && prevEvent.getContent().msgtype === 'm.emote') { - continuation = false; - } -*/ // local echoes have a fake date, which could even be yesterday. Treat them // as 'today' for the date separators. @@ -571,12 +550,15 @@ export default class MessagePanel extends React.Component { } // do we need a date separator since the last event? - if (this._wantsDateSeparator(prevEvent, eventDate)) { + const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); + if (wantsDateSeparator) { const dateSeparator =
  • ; ret.push(dateSeparator); - continuation = false; } + // is this a continuation of the previous message? + const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv); + const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); @@ -614,7 +596,7 @@ export default class MessagePanel extends React.Component { isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} - useIRCLayout={this.state.useIRCLayout} + useIRCLayout={this.props.useIRCLayout} /> , @@ -797,8 +779,6 @@ export default class MessagePanel extends React.Component { this.props.className, { "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, - "mx_IRCLayout": this.state.useIRCLayout, - "mx_GroupLayout": !this.state.useIRCLayout, }, ); @@ -813,11 +793,11 @@ export default class MessagePanel extends React.Component { } let ircResizer = null; - if (this.state.useIRCLayout) { + if (this.props.useIRCLayout) { ircResizer = ; } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 56cc92a8f8..776130e709 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -26,7 +26,6 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; -import SettingsStore from "../../settings/SettingsStore"; import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -189,16 +188,37 @@ export default class RightPanel extends React.Component { } } + onCloseUserInfo = () => { + // XXX: There are three different ways of 'closing' this panel depending on what state + // things are in... this knows far more than it should do about the state of the rest + // of the app and is generally a bit silly. + if (this.props.user) { + // If we have a user prop then we're displaying a user from the 'user' page type + // in LoggedInView, so need to change the page type to close the panel (we switch + // to the home page which is not obviously the correct thing to do, but I'm not sure + // anything else is - we could hide the close button altogether?) + dis.dispatch({ + action: "view_home_page", + }); + } else { + // Otherwise we have got our user from RoomViewStore which means we're being shown + // within a room/group, so go back to the member panel if we were in the encryption panel, + // or the member list if we were in the member panel... phew. + dis.dispatch({ + action: Action.ViewUser, + member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, + }); + } + }; + render() { const MemberList = sdk.getComponent('rooms.MemberList'); - const MemberInfo = sdk.getComponent('rooms.MemberInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const FilePanel = sdk.getComponent('structures.FilePanel'); const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); - const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo'); const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); @@ -220,71 +240,25 @@ export default class RightPanel extends React.Component { break; case RIGHT_PANEL_PHASES.RoomMemberInfo: case RIGHT_PANEL_PHASES.EncryptionPanel: - if (SettingsStore.getValue("feature_cross_signing")) { - const onClose = () => { - // XXX: There are three different ways of 'closing' this panel depending on what state - // things are in... this knows far more than it should do about the state of the rest - // of the app and is generally a bit silly. - if (this.props.user) { - // If we have a user prop then we're displaying a user from the 'user' page type - // in LoggedInView, so need to change the page type to close the panel (we switch - // to the home page which is not obviously the correct thing to do, but I'm not sure - // anything else is - we could hide the close button altogether?) - dis.dispatch({ - action: "view_home_page", - }); - } else { - // Otherwise we have got our user from RoomViewStore which means we're being shown - // within a room, so go back to the member panel if we were in the encryption panel, - // or the member list if we were in the member panel... phew. - dis.dispatch({ - action: Action.ViewUser, - member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? - this.state.member : null, - }); - } - }; - panel = ; - } else { - panel = ; - } + panel = ; break; case RIGHT_PANEL_PHASES.Room3pidMemberInfo: panel = ; break; case RIGHT_PANEL_PHASES.GroupMemberInfo: - if (SettingsStore.getValue("feature_cross_signing")) { - const onClose = () => { - dis.dispatch({ - action: Action.ViewUser, - member: null, - }); - }; - panel = ; - } else { - panel = ( - - ); - } + panel = ; break; case RIGHT_PANEL_PHASES.GroupRoomInfo: panel = { if (!alias) return; - step = _t('delete the alias.'); + step = _t('delete the address.'); return MatrixClientPeg.get().deleteAlias(alias); }).then(() => { modal.close(); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index ae628fd06a..65d062cfaa 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler'; import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; -import * as cryptodevices from '../../cryptodevices'; import dis from '../../dispatcher/dispatcher'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; +import {Action} from "../../dispatcher/actions"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -126,25 +126,14 @@ export default createReactClass({ }); }, - _onSendWithoutVerifyingClick: function() { - cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => { - cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices); - Resend.resendUnsentEvents(this.props.room); - }); - }, - _onResendAllClick: function() { Resend.resendUnsentEvents(this.props.room); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, _onCancelAllClick: function() { Resend.cancelUnsentEvents(this.props.room); - dis.dispatch({action: 'focus_composer'}); - }, - - _onShowDevicesClick: function() { - cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); + dis.fire(Action.FocusComposer); }, _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { @@ -213,82 +202,65 @@ export default createReactClass({ if (!unsentMessages.length) return null; let title; - let content; - const hasUDE = unsentMessages.some((m) => { - return m.error && m.error.name === "UnknownDeviceError"; - }); - - if (hasUDE) { - title = _t("Message not sent due to unknown sessions being present"); - content = _t( - "Show sessions, send anyway or cancel.", + let consentError = null; + let resourceLimitError = null; + for (const m of unsentMessages) { + if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') { + consentError = m.error; + break; + } else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { + resourceLimitError = m.error; + break; + } + } + if (consentError) { + title = _t( + "You can't send any messages until you review and agree to " + + "our terms and conditions.", {}, { - 'showSessionsText': (sub) => { sub }, - 'sendAnywayText': (sub) => { sub }, - 'cancelText': (sub) => { sub }, + 'consentLink': (sub) => + + { sub } + , }, ); + } else if (resourceLimitError) { + title = messageForResourceLimitError( + resourceLimitError.data.limit_type, + resourceLimitError.data.admin_contact, { + 'monthly_active_user': _td( + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + + "Please contact your service administrator to continue using the service.", + ), + '': _td( + "Your message wasn't sent because this homeserver has exceeded a resource limit. " + + "Please contact your service administrator to continue using the service.", + ), + }); + } else if ( + unsentMessages.length === 1 && + unsentMessages[0].error && + unsentMessages[0].error.data && + unsentMessages[0].error.data.error + ) { + title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; } else { - let consentError = null; - let resourceLimitError = null; - for (const m of unsentMessages) { - if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') { - consentError = m.error; - break; - } else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { - resourceLimitError = m.error; - break; - } - } - if (consentError) { - title = _t( - "You can't send any messages until you review and agree to " + - "our terms and conditions.", - {}, - { - 'consentLink': (sub) => - - { sub } - , - }, - ); - } else if (resourceLimitError) { - title = messageForResourceLimitError( - resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, { - 'monthly_active_user': _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", - ), - '': _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", - ), - }); - } else if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error - ) { - title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; - } else { - title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); - } - content = _t("%(count)s Resend all or cancel all now. " + - "You can also select individual messages to resend or cancel.", - { count: unsentMessages.length }, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ); + title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); } + const content = _t("%(count)s Resend all or cancel all " + + "now. You can also select individual messages to resend or cancel.", + { count: unsentMessages.length }, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, + ); + return
    diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 635597db74..0ff997ee09 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; +import {Action} from "../../dispatcher/actions"; const DEBUG = false; let debuglog = function() {}; @@ -164,6 +165,10 @@ export default createReactClass({ canReact: false, canReply: false, + + useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + + matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; }, @@ -193,6 +198,8 @@ export default createReactClass({ this._roomView = createRef(); this._searchResultsPanel = createRef(); + + this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); }, _onReadReceiptsChange: function() { @@ -232,7 +239,8 @@ export default createReactClass({ initialEventId: RoomViewStore.getInitialEventId(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), forwardingEvent: RoomViewStore.getForwardingEvent(), - shouldPeek: RoomViewStore.shouldPeek(), + // we should only peek once we have a ready client + shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), }; @@ -532,6 +540,14 @@ export default createReactClass({ // no need to do this as Dir & Settings are now overlays. It just burnt CPU. // console.log("Tinter.tint from RoomView.unmount"); // Tinter.tint(); // reset colourscheme + + SettingsStore.unwatchSetting(this._layoutWatcherRef); + }, + + onLayoutChange: function() { + this.setState({ + useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + }); }, _onRightPanelStoreUpdate: function() { @@ -681,6 +697,16 @@ export default createReactClass({ }); } break; + case 'sync_state': + if (!this.state.matrixClientIsReady) { + this.setState({ + matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), + }, () => { + // send another "initial" RVS update to trigger peeking if needed + this._onRoomViewStoreUpdate(true); + }); + } + break; } }, @@ -854,15 +880,6 @@ export default createReactClass({ }); return; } - if (!SettingsStore.getValue("feature_cross_signing")) { - room.hasUnverifiedDevices().then((hasUnverifiedDevices) => { - this.setState({ - e2eStatus: hasUnverifiedDevices ? "warning" : "verified", - }); - }); - debuglog("e2e check is warning/verified only as cross-signing is off"); - return; - } /* At this point, the user has encryption on and cross-signing on */ this.setState({ @@ -1146,7 +1163,7 @@ export default createReactClass({ ev.dataTransfer.files, this.state.room.roomId, this.context, ); this.setState({ draggingFile: false }); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, onDragLeaveOrEnd: function(ev) { @@ -1352,7 +1369,7 @@ export default createReactClass({ event: null, }); } - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, onLeaveClick: function() { @@ -1463,7 +1480,7 @@ export default createReactClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { this._messagePanel.jumpToLiveTimeline(); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, // jump up to wherever our read marker is @@ -1663,14 +1680,16 @@ export default createReactClass({ const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary"); if (!this.state.room) { - const loading = this.state.roomLoading || this.state.peekLoading; + const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading; if (loading) { + // Assume preview loading if we don't have a ready client or a room ID (still resolving the alias) + const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading; return (
    - ); let topUnreadMessagesBar = null; diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js deleted file mode 100644 index 4b00da3cbf..0000000000 --- a/src/components/structures/TagPanelButtons.js +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2019 New Vector Ltd. - -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 createReactClass from 'create-react-class'; -import * as sdk from '../../index'; -import dis from '../../dispatcher/dispatcher'; -import Modal from '../../Modal'; -import { _t } from '../../languageHandler'; - -const TagPanelButtons = createReactClass({ - displayName: 'TagPanelButtons', - - - componentDidMount: function() { - this._dispatcherRef = dis.register(this._onAction); - }, - - componentWillUnmount() { - if (this._dispatcherRef) { - dis.unregister(this._dispatcherRef); - this._dispatcherRef = null; - } - }, - - _onAction(payload) { - if (payload.action === "show_redesign_feedback_dialog") { - const RedesignFeedbackDialog = - sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); - Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - } - }, - - render() { - const GroupsButton = sdk.getComponent('elements.GroupsButton'); - const ActionButton = sdk.getComponent("elements.ActionButton"); - - return (
    - - -
    ); - }, -}); -export default TagPanelButtons; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index da1369c45f..95dc42fcee 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({ // whether to show reactions for an event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }, statics: { @@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({ getRelationsForEvent={this.getRelationsForEvent} editState={this.state.editState} showReactions={this.props.showReactions} + useIRCLayout={this.props.useIRCLayout} /> ); }, diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.tsx similarity index 69% rename from src/components/structures/ToastContainer.js rename to src/components/structures/ToastContainer.tsx index 283fbdd96a..84473031fa 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.tsx @@ -15,14 +15,21 @@ limitations under the License. */ import * as React from "react"; -import { _t } from '../../languageHandler'; -import ToastStore from "../../stores/ToastStore"; +import ToastStore, {IToast} from "../../stores/ToastStore"; import classNames from "classnames"; -export default class ToastContainer extends React.Component { - constructor() { - super(); - this.state = {toasts: ToastStore.sharedInstance().getToasts()}; +interface IState { + toasts: IToast[]; + countSeen: number; +} + +export default class ToastContainer extends React.Component<{}, IState> { + constructor(props, context) { + super(props, context); + this.state = { + toasts: ToastStore.sharedInstance().getToasts(), + countSeen: ToastStore.sharedInstance().getCountSeen(), + }; // Start listening here rather than in componentDidMount because // toasts may dismiss themselves in their didMount if they find @@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component { } _onToastStoreUpdate = () => { - this.setState({toasts: ToastStore.sharedInstance().getToasts()}); + this.setState({ + toasts: ToastStore.sharedInstance().getToasts(), + countSeen: ToastStore.sharedInstance().getCountSeen(), + }); }; render() { @@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, }); - const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + let countIndicator; + if (isStacked || this.state.countSeen > 0) { + countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; + } const toastProps = Object.assign({}, props, { key, toastKey: key, }); toast = (
    -

    {title}{countIndicator}

    +
    +

    {title}

    + {countIndicator} +
    {React.createElement(component, toastProps)}
    ); } diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 7be88f9d44..6349614d72 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -247,9 +247,8 @@ export default createReactClass({ // do SSO instead. If we've already started the UI Auth process though, we don't // need to. if (!this.state.doingUIAuth) { - await this._makeRegisterRequest({}); - // This should never succeed since we specified an empty - // auth object. + await this._makeRegisterRequest(null); + // This should never succeed since we specified no auth object. console.log("Expecting 401 from register request but got success!"); } } catch (e) { diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 5d9f868f85..a2824b63a3 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -25,6 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import SSOButton from "../../views/elements/SSOButton"; +import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform"; const LOGIN_VIEW = { LOADING: 1, @@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = { export default class SoftLogout extends React.Component { static propTypes = { // Query parameters from MatrixChat - realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken} + realQueryParams: PropTypes.object, // {loginToken} // Called when the SSO login completes onTokenLoginCompleted: PropTypes.func, @@ -90,7 +91,7 @@ export default class SoftLogout extends React.Component { async _initLogin() { const queryParams = this.props.realQueryParams; - const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken']; + const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { this.setState({loginView: LOGIN_VIEW.LOADING}); this.trySsoLogin(); @@ -157,8 +158,8 @@ export default class SoftLogout extends React.Component { async trySsoLogin() { this.setState({busy: true}); - const hsUrl = this.props.realQueryParams['homeserver']; - const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl(); + const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl(); const loginType = "m.login.token"; const loginParams = { token: this.props.realQueryParams['loginToken'], diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 655452fcee..f6bc1b8ae7 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({ allChecked = allChecked && checked; checkboxes.push( + // XXX: replace with StyledCheckbox
    ); diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js index bc5dec1468..5bdfdde08d 100644 --- a/src/components/views/create_room/RoomAlias.js +++ b/src/components/views/create_room/RoomAlias.js @@ -98,7 +98,7 @@ export default createReactClass({ render: function() { return ( - ); diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 67d70aabe4..e59b6bbaf5 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -144,6 +144,7 @@ export default createReactClass({ >
    {headerImage} diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index fb08afa5f7..ce7ac6e59c 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -24,7 +24,7 @@ import withValidation from '../elements/Validation'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; -import SettingsStore from "../../../settings/SettingsStore"; +import {privateShouldBeEncrypted} from "../../../createRoom"; export default createReactClass({ displayName: 'CreateRoomDialog', @@ -37,7 +37,7 @@ export default createReactClass({ const config = SdkConfig.get(); return { isPublic: this.props.defaultPublic || false, - isEncrypted: true, + isEncrypted: privateShouldBeEncrypted(), name: "", topic: "", alias: "", @@ -66,7 +66,7 @@ export default createReactClass({ createOpts.creation_content = {'m.federate': false}; } - if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) { + if (!this.state.isPublic) { opts.encryption = this.state.isEncrypted; } @@ -181,7 +181,7 @@ export default createReactClass({ let publicPrivateLabel; let aliasField; if (this.state.isPublic) { - publicPrivateLabel = (

    {_t("Set a room alias to easily share your room with other people.")}

    ); + publicPrivateLabel = (

    {_t("Set a room address to easily share your room with other people.")}

    ); const domain = MatrixClientPeg.get().getDomain(); aliasField = (
    @@ -193,7 +193,14 @@ export default createReactClass({ } let e2eeSection; - if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) { + if (!this.state.isPublic) { + let microcopy; + if (privateShouldBeEncrypted()) { + microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet."); + } else { + microcopy = _t("Your server admin has disabled end-to-end encryption by default " + + "in private rooms & Direct Messages."); + } e2eeSection = -

    { _t("You can’t disable this later. Bridges & most bots won’t work yet.") }

    +

    { microcopy }

    ; } diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.js index 081e84696c..4694619601 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.js @@ -42,11 +42,9 @@ export default (props) => { }; const description = - _t("You've previously used a newer version of Riot on %(host)s. " + + _t("You've previously used a newer version of Riot with this session. " + "To use this version again with end to end encryption, you will " + - "need to sign out and back in again. ", - {host: props.host}, - ); + "need to sign out and back in again."); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index ffef2a30c7..fca8c42546 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -25,6 +25,7 @@ import * as Lifecycle from '../../../Lifecycle'; import { _t } from '../../../languageHandler'; import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; +import StyledCheckbox from "../elements/StyledCheckbox"; export default class DeactivateAccountDialog extends React.Component { constructor(props) { @@ -209,21 +210,18 @@ export default class DeactivateAccountDialog extends React.Component {

    - + )} +

    {error} diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js deleted file mode 100644 index 51f905d542..0000000000 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ /dev/null @@ -1,377 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019 New Vector Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -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 PropTypes from 'prop-types'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; -import * as FormattingUtils from '../../../utils/FormattingUtils'; -import { _t } from '../../../languageHandler'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import {ensureDMExists} from "../../../createRoom"; -import dis from "../../../dispatcher/dispatcher"; -import SettingsStore from '../../../settings/SettingsStore'; -import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; -import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions"; - -const MODE_LEGACY = 'legacy'; -const MODE_SAS = 'sas'; - -const PHASE_START = 0; -const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1; -const PHASE_PICK_VERIFICATION_OPTION = 2; -const PHASE_SHOW_SAS = 3; -const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4; -const PHASE_VERIFIED = 5; -const PHASE_CANCELLED = 6; - -export default class DeviceVerifyDialog extends React.Component { - static propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - constructor() { - super(); - this._verifier = null; - this._showSasEvent = null; - this._request = null; - this.state = { - phase: PHASE_START, - mode: MODE_SAS, - sasVerified: false, - }; - } - - componentWillUnmount() { - if (this._verifier) { - this._verifier.removeListener('show_sas', this._onVerifierShowSas); - this._verifier.cancel('User cancel'); - } - } - - _onSwitchToLegacyClick = () => { - if (this._verifier) { - this._verifier.removeListener('show_sas', this._onVerifierShowSas); - this._verifier.cancel('User cancel'); - this._verifier = null; - } - this.setState({mode: MODE_LEGACY}); - } - - _onSwitchToSasClick = () => { - this.setState({mode: MODE_SAS}); - } - - _onCancelClick = () => { - this.props.onFinished(false); - } - - _onUseSasClick = async () => { - try { - this._verifier = this._request.beginKeyVerification(verificationMethods.SAS); - this._verifier.on('show_sas', this._onVerifierShowSas); - // throws upon cancellation - await this._verifier.verify(); - this.setState({phase: PHASE_VERIFIED}); - this._verifier.removeListener('show_sas', this._onVerifierShowSas); - this._verifier = null; - } catch (e) { - console.log("Verification failed", e); - this.setState({ - phase: PHASE_CANCELLED, - }); - this._verifier = null; - this._request = null; - } - }; - - _onLegacyFinished = (confirm) => { - if (confirm) { - MatrixClientPeg.get().setDeviceVerified( - this.props.userId, this.props.device.deviceId, true, - ); - } - this.props.onFinished(confirm); - } - - _onSasRequestClick = async () => { - this.setState({ - phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT, - }); - const client = MatrixClientPeg.get(); - const verifyingOwnDevice = this.props.userId === client.getUserId(); - try { - if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) { - const roomId = await ensureDMExistsAndOpen(this.props.userId); - // throws upon cancellation before having started - const request = await client.requestVerificationDM( - this.props.userId, roomId, - ); - await request.waitFor(r => r.ready || r.started); - if (request.ready) { - this._verifier = request.beginKeyVerification(verificationMethods.SAS); - } else { - this._verifier = request.verifier; - } - } else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) { - this._request = await client.requestVerification(this.props.userId, [ - verificationMethods.SAS, - SHOW_QR_CODE_METHOD, - verificationMethods.RECIPROCATE_QR_CODE, - ]); - - await this._request.waitFor(r => r.ready || r.started); - this.setState({phase: PHASE_PICK_VERIFICATION_OPTION}); - } else { - this._verifier = client.beginKeyVerification( - verificationMethods.SAS, this.props.userId, this.props.device.deviceId, - ); - } - if (!this._verifier) return; - this._verifier.on('show_sas', this._onVerifierShowSas); - // throws upon cancellation - await this._verifier.verify(); - this.setState({phase: PHASE_VERIFIED}); - this._verifier.removeListener('show_sas', this._onVerifierShowSas); - this._verifier = null; - } catch (e) { - console.log("Verification failed", e); - this.setState({ - phase: PHASE_CANCELLED, - }); - this._verifier = null; - } - } - - _onSasMatchesClick = () => { - this._showSasEvent.confirm(); - this.setState({ - phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, - }); - } - - _onVerifiedDoneClick = () => { - this.props.onFinished(true); - } - - _onVerifierShowSas = (e) => { - this._showSasEvent = e; - this.setState({ - phase: PHASE_SHOW_SAS, - }); - } - - _renderSasVerification() { - let body; - switch (this.state.phase) { - case PHASE_START: - body = this._renderVerificationPhaseStart(); - break; - case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT: - body = this._renderVerificationPhaseWaitAccept(); - break; - case PHASE_PICK_VERIFICATION_OPTION: - body = this._renderVerificationPhasePick(); - break; - case PHASE_SHOW_SAS: - body = this._renderSasVerificationPhaseShowSas(); - break; - case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: - body = this._renderSasVerificationPhaseWaitForPartnerToConfirm(); - break; - case PHASE_VERIFIED: - body = this._renderVerificationPhaseVerified(); - break; - case PHASE_CANCELLED: - body = this._renderVerificationPhaseCancelled(); - break; - } - - const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); - return ( - - {body} - - ); - } - - _renderVerificationPhaseStart() { - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( -
    - - {_t("Use Legacy Verification (for older clients)")} - -

    - { _t("Verify by comparing a short text string.") } -

    -

    - {_t("To be secure, do this in person or use a trusted way to communicate.")} -

    - -
    - ); - } - - _renderVerificationPhaseWaitAccept() { - const Spinner = sdk.getComponent("views.elements.Spinner"); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - - return ( -
    - -

    {_t("Waiting for partner to accept...")}

    -

    {_t( - "Nothing appearing? Not all clients support interactive verification yet. " + - ".", - {}, {button: sub => - {sub} - }, - )}

    -
    - ); - } - - _renderVerificationPhasePick() { - return ; - } - - _renderSasVerificationPhaseShowSas() { - const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); - return ; - } - - _renderSasVerificationPhaseWaitForPartnerToConfirm() { - const Spinner = sdk.getComponent('views.elements.Spinner'); - return
    - -

    {_t( - "Waiting for %(userId)s to confirm...", {userId: this.props.userId}, - )}

    -
    ; - } - - _renderVerificationPhaseVerified() { - const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); - return ; - } - - _renderVerificationPhaseCancelled() { - const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); - return ; - } - - _renderLegacyVerification() { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - - let text; - if (MatrixClientPeg.get().getUserId() === this.props.userId) { - text = _t("To verify that this session can be trusted, please check that the key you see " + - "in User Settings on that device matches the key below:"); - } else { - text = _t("To verify that this session can be trusted, please contact its owner using some other " + - "means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " + - "for this session matches the key below:"); - } - - const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint()); - const body = ( -
    - - {_t("Use two-way text verification")} - -

    - { text } -

    -
    -
      -
    • { this.props.device.getDisplayName() }
    • -
    • { this.props.device.deviceId }
    • -
    • { key }
    • -
    -
    -

    - { _t("If it matches, press the verify button below. " + - "If it doesn't, then someone else is intercepting this session " + - "and you probably want to press the blacklist button instead.") } -

    -
    - ); - - return ( - - ); - } - - render() { - if (this.state.mode === MODE_LEGACY) { - return this._renderLegacyVerification(); - } else { - return
    - {this._renderSasVerification()} -
    ; - } - } -} - -async function ensureDMExistsAndOpen(userId) { - const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); - // don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen, - // we causes us to loose the verifier and restart, and we end up having two verification requests - dis.dispatch({ - action: 'view_room', - room_id: roomId, - should_peek: false, - }); - return roomId; -} diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 4274c938cc..7ac9e21518 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -31,9 +31,8 @@ import dis from "../../../dispatcher/dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; -import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; +import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; -import SettingsStore from '../../../settings/SettingsStore'; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; @@ -576,7 +575,7 @@ export default class InviteDialog extends React.PureComponent { const createRoomOptions = {inlineErrors: true}; - if (SettingsStore.getValue("feature_cross_signing")) { + if (privateShouldBeEncrypted()) { // Check whether all users have uploaded device keys before. // If so, enable encryption in the new room. const has3PidMembers = targets.some(t => t instanceof ThreepidMember); diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js deleted file mode 100644 index 8ef36bb59f..0000000000 --- a/src/components/views/dialogs/KeyShareDialog.js +++ /dev/null @@ -1,178 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -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 Modal from '../../../Modal'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; - -import { _t, _td } from '../../../languageHandler'; - -// TODO: We can remove this once cross-signing is the only way. -// https://github.com/vector-im/riot-web/issues/11908 - -/** - * Dialog which asks the user whether they want to share their keys with - * an unverified device. - * - * onFinished is called with `true` if the key should be shared, `false` if it - * should not, and `undefined` if the dialog is cancelled. (In other words: - * truthy: do the key share. falsy: don't share the keys). - */ -export default createReactClass({ - propTypes: { - matrixClient: PropTypes.object.isRequired, - userId: PropTypes.string.isRequired, - deviceId: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, - }, - - getInitialState: function() { - return { - deviceInfo: null, - wasNewDevice: false, - }; - }, - - componentDidMount: function() { - this._unmounted = false; - const userId = this.props.userId; - const deviceId = this.props.deviceId; - - // give the client a chance to refresh the device list - this.props.matrixClient.downloadKeys([userId], false).then((r) => { - if (this._unmounted) { return; } - - const deviceInfo = r[userId][deviceId]; - - if (!deviceInfo) { - console.warn(`No details found for session ${userId}:${deviceId}`); - - this.props.onFinished(false); - return; - } - - const wasNewDevice = !deviceInfo.isKnown(); - - this.setState({ - deviceInfo: deviceInfo, - wasNewDevice: wasNewDevice, - }); - - // if the device was new before, it's not any more. - if (wasNewDevice) { - this.props.matrixClient.setDeviceKnown( - userId, - deviceId, - true, - ); - } - }); - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - - _onVerifyClicked: function() { - const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); - - console.log("KeyShareDialog: Starting verify dialog"); - Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, { - userId: this.props.userId, - device: this.state.deviceInfo, - onFinished: (verified) => { - if (verified) { - // can automatically share the keys now. - this.props.onFinished(true); - } - }, - }, null, /* priority = */ false, /* static = */ true); - }, - - _onShareClicked: function() { - console.log("KeyShareDialog: User clicked 'share'"); - this.props.onFinished(true); - }, - - _onIgnoreClicked: function() { - console.log("KeyShareDialog: User clicked 'ignore'"); - this.props.onFinished(false); - }, - - _renderContent: function() { - const displayName = this.state.deviceInfo.getDisplayName() || - this.state.deviceInfo.deviceId; - - let text; - if (this.state.wasNewDevice) { - text = _td("You added a new session '%(displayName)s', which is" - + " requesting encryption keys."); - } else { - text = _td("Your unverified session '%(displayName)s' is requesting" - + " encryption keys."); - } - text = _t(text, {displayName: displayName}); - - return ( -
    -

    { text }

    - -
    - - - -
    -
    - ); - }, - - render: function() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Spinner = sdk.getComponent('views.elements.Spinner'); - - let content; - - if (this.state.deviceInfo) { - content = this._renderContent(); - } else { - content = ( -
    -

    { _t('Loading session info...') }

    - -
    - ); - } - - return ( - - { content } - - ); - }, -}); diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index 271f754fd2..2e1529cbf1 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -29,6 +29,7 @@ import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../.. import * as ContextMenu from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext, selectText} from "../../../utils/strings"; +import StyledCheckbox from '../elements/StyledCheckbox'; const socials = [ { @@ -168,13 +169,12 @@ export default class ShareDialog extends React.PureComponent { const events = this.props.target.getLiveTimeline().getEvents(); if (events.length > 0) { checkbox =
    - - +
    ; } } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { @@ -184,13 +184,12 @@ export default class ShareDialog extends React.PureComponent { } else if (this.props.target instanceof MatrixEvent) { title = _t('Share Room Message'); checkbox =
    - - +
    ; } diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js deleted file mode 100644 index 4cad13b047..0000000000 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd - -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 createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import { _t } from '../../../languageHandler'; -import SettingsStore from "../../../settings/SettingsStore"; -import { markAllDevicesKnown } from '../../../cryptodevices'; - -function UserUnknownDeviceList(props) { - const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); - const {userId, userDevices} = props; - - const deviceListEntries = Object.keys(userDevices).map((deviceId) => -
  • , - ); - - return ( -
      - { deviceListEntries } -
    - ); -} - -UserUnknownDeviceList.propTypes = { - userId: PropTypes.string.isRequired, - - // map from deviceid -> deviceinfo - userDevices: PropTypes.object.isRequired, -}; - - -function UnknownDeviceList(props) { - const {devices} = props; - - const userListEntries = Object.keys(devices).map((userId) => -
  • -

    { userId }:

    - -
  • , - ); - - return
      { userListEntries }
    ; -} - -UnknownDeviceList.propTypes = { - // map from userid -> deviceid -> deviceinfo - devices: PropTypes.object.isRequired, -}; - - -export default createReactClass({ - displayName: 'UnknownDeviceDialog', - - propTypes: { - room: PropTypes.object.isRequired, - - // map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded - devices: PropTypes.object, - - onFinished: PropTypes.func.isRequired, - - // Label for the button that marks all devices known and tries the send again - sendAnywayLabel: PropTypes.string.isRequired, - - // Label for the button that to send the event if you've verified all devices - sendLabel: PropTypes.string.isRequired, - - // function to retry the request once all devices are verified / known - onSend: PropTypes.func.isRequired, - }, - - componentDidMount: function() { - MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged); - }, - - componentWillUnmount: function() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged); - } - }, - - _onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) { - if (this.props.devices[userId] && this.props.devices[userId][deviceId]) { - // XXX: Mutating props :/ - this.props.devices[userId][deviceId] = deviceInfo; - this.forceUpdate(); - } - }, - - _onDismissClicked: function() { - this.props.onFinished(); - }, - - _onSendAnywayClicked: function() { - markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices); - - this.props.onFinished(); - this.props.onSend(); - }, - - _onSendClicked: function() { - this.props.onFinished(); - this.props.onSend(); - }, - - render: function() { - if (this.props.devices === null) { - const Spinner = sdk.getComponent("elements.Spinner"); - return ; - } - - let warning; - if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) { - warning = ( -

    - { _t("You are currently blacklisting unverified sessions; to send " + - "messages to these sessions you must verify them.") } -

    - ); - } else { - warning = ( -
    -

    - { _t("We recommend you go through the verification process " + - "for each session to confirm they belong to their legitimate owner, " + - "but you can resend the message without verifying if you prefer.") } -

    -
    - ); - } - - let haveUnknownDevices = false; - Object.keys(this.props.devices).forEach((userId) => { - Object.keys(this.props.devices[userId]).map((deviceId) => { - const device = this.props.devices[userId][deviceId]; - if (device.isUnverified() && !device.isKnown()) { - haveUnknownDevices = true; - } - }); - }); - const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked; - const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel; - - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( - -
    -

    - { _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) } -

    - { warning } - { _t("Unknown sessions") }: - - -
    - -
    - ); - // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point? - // It feels like confused users will likely turn it on and then disappear in a cloud of UISIs... - }, -}); diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.js index 28f2183eb0..e3521eb282 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.js +++ b/src/components/views/dialogs/UploadConfirmDialog.js @@ -84,7 +84,7 @@ export default class UploadConfirmDialog extends React.Component { preview =
    {this.props.file.name} ({filesize(this.props.file.size)})
    diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index a16202ed93..dd34dfbbf0 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -20,10 +20,8 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import { MatrixClient } from 'matrix-js-sdk'; -import Modal from '../../../../Modal'; import { _t } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; -import SettingsStore from "../../../../settings/SettingsStore"; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; @@ -90,21 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { _onResetRecoveryClick = () => { this.props.onFinished(false); - - if (SettingsStore.getValue("feature_cross_signing")) { - // If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys) - this.props.onFinished(false); - accessSecretStorage(() => {}, /* forceReset = */ true); - } else { - Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', - import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), - { - onFinished: () => { - this._loadBackupStatus(); - }, - }, null, /* priority = */ false, /* static = */ true, - ); - } + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { @@ -243,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { loadError: null, }); try { - const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); + const cli = MatrixClientPeg.get(); + const backupInfo = await cli.getKeyBackupVersion(); + const has4S = await cli.hasSecretStorageKey(); + const backupKeyStored = has4S && await cli.isKeyBackupKeyStored(); this.setState({ backupInfo, backupKeyStored, diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js deleted file mode 100644 index 7328d50328..0000000000 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; -import Modal from '../../../Modal'; -import { _t } from '../../../languageHandler'; - -// XXX: This component is *not* cross-signing aware. Once everything is -// cross-signing, this component should just go away. -export default createReactClass({ - displayName: 'DeviceVerifyButtons', - - propTypes: { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - }, - - getInitialState: function() { - return { - device: this.props.device, - }; - }, - - componentDidMount: function() { - const cli = MatrixClientPeg.get(); - cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); - }, - - componentWillUnmount: function() { - const cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - } - }, - - onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) { - if (userId === this.props.userId && deviceId === this.props.device.deviceId) { - this.setState({ device: deviceInfo }); - } - }, - - onVerifyClick: function() { - const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); - Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { - userId: this.props.userId, - device: this.state.device, - }, null, /* priority = */ false, /* static = */ true); - }, - - onUnverifyClick: function() { - MatrixClientPeg.get().setDeviceVerified( - this.props.userId, this.state.device.deviceId, false, - ); - }, - - onBlacklistClick: function() { - MatrixClientPeg.get().setDeviceBlocked( - this.props.userId, this.state.device.deviceId, true, - ); - }, - - onUnblacklistClick: function() { - MatrixClientPeg.get().setDeviceBlocked( - this.props.userId, this.state.device.deviceId, false, - ); - }, - - render: function() { - let blacklistButton = null; let verifyButton = null; - - if (this.state.device.isBlocked()) { - blacklistButton = ( - - ); - } else { - blacklistButton = ( - - ); - } - - if (this.state.device.isVerified()) { - verifyButton = ( - - ); - } else { - verifyButton = ( - - ); - } - - return ( -
    - { verifyButton } - { blacklistButton } -
    - ); - }, -}); diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 98f86fd524..3096ac42f7 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -58,18 +58,15 @@ export default class Draggable extends React.Component { document.addEventListener("mousemove", this.state.onMouseMove); document.addEventListener("mouseup", this.state.onMouseUp); - console.log("Mouse down") } private onMouseUp = (event: MouseEvent): void => { document.removeEventListener("mousemove", this.state.onMouseMove); document.removeEventListener("mouseup", this.state.onMouseUp); this.props.onMouseUp(event); - console.log("Mouse up") } private onMouseMove(event: MouseEvent): void { - console.log("Mouse Move") const newLocation = this.props.dragFunc(this.state.location, event); this.setState({ diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.tsx similarity index 64% rename from src/components/views/elements/Field.js rename to src/components/views/elements/Field.tsx index 2ebb90da26..771d2182ea 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from '../../../index'; import { debounce } from 'lodash'; +import {IFieldState, IValidationResult} from "../elements/Validation"; // Invoke validation from user input (when typing, etc.) at most once every N ms. const VALIDATION_THROTTLE_MS = 200; @@ -29,58 +29,93 @@ function getId() { return `${BASE_ID}_${count++}`; } -export default class Field extends React.PureComponent { - static propTypes = { - // The field's ID, which binds the input and label together. Immutable. - id: PropTypes.string, - // The element to create. Defaults to "input". - // To define options for a select, use - element: PropTypes.oneOf(["input", "select", "textarea"]), - // The field's type (when used as an ). Defaults to "text". - type: PropTypes.string, - // id of a element for suggestions - list: PropTypes.string, - // The field's label string. - label: PropTypes.string, - // The field's placeholder string. Defaults to the label. - placeholder: PropTypes.string, - // The field's value. - // This is a controlled component, so the value is required. - value: PropTypes.string.isRequired, - // Optional component to include inside the field before the input. - prefix: PropTypes.node, - // Optional component to include inside the field after the input. - postfix: PropTypes.node, - // The callback called whenever the contents of the field - // changes. Returns an object with `valid` boolean field - // and a `feedback` react component field to provide feedback - // to the user. - onValidate: PropTypes.func, - // If specified, overrides the value returned by onValidate. - flagInvalid: PropTypes.bool, - // If specified, contents will appear as a tooltip on the element and - // validation feedback tooltips will be suppressed. - tooltipContent: PropTypes.node, - // If specified alongside tooltipContent, the class name to apply to the - // tooltip itself. - tooltipClassName: PropTypes.string, - // If specified, an additional class name to apply to the field container - className: PropTypes.string, - // All other props pass through to the . - }; +interface IProps extends React.InputHTMLAttributes { + // The field's ID, which binds the input and label together. Immutable. + id?: string, + // The element to create. Defaults to "input". + // To define options for a select, use + element?: "input" | "select" | "textarea", + // The field's type (when used as an ). Defaults to "text". + type?: string, + // id of a element for suggestions + list?: string, + // The field's label string. + label?: string, + // The field's placeholder string. Defaults to the label. + placeholder?: string, + // The field's value. + // This is a controlled component, so the value is required. + value: string, + // Optional component to include inside the field before the input. + prefixComponent?: React.ReactNode, + // Optional component to include inside the field after the input. + postfixComponent?: React.ReactNode, + // The callback called whenever the contents of the field + // changes. Returns an object with `valid` boolean field + // and a `feedback` react component field to provide feedback + // to the user. + onValidate?: (input: IFieldState) => Promise, + // If specified, overrides the value returned by onValidate. + flagInvalid?: boolean, + // If specified, contents will appear as a tooltip on the element and + // validation feedback tooltips will be suppressed. + tooltipContent?: React.ReactNode, + // If specified alongside tooltipContent, the class name to apply to the + // tooltip itself. + tooltipClassName?: string, + // If specified, an additional class name to apply to the field container + className?: string, + // All other props pass through to the . +} + +interface IState { + valid: boolean, + feedback: React.ReactNode, + feedbackVisible: boolean, + focused: boolean, +} + +export default class Field extends React.PureComponent { + private id: string; + private input: HTMLInputElement; + + private static defaultProps = { + element: "input", + type: "text", + } + + /* + * This was changed from throttle to debounce: this is more traditional for + * form validation since it means that the validation doesn't happen at all + * until the user stops typing for a bit (debounce defaults to not running on + * the leading edge). If we're doing an HTTP hit on each validation, we have more + * incentive to prevent validating input that's very unlikely to be valid. + * We may find that we actually want different behaviour for registration + * fields, in which case we can add some options to control it. + */ + private validateOnChange = debounce(() => { + this.validate({ + focused: true, + }); + }, VALIDATION_THROTTLE_MS); constructor(props) { super(props); this.state = { valid: undefined, feedback: undefined, + feedbackVisible: false, focused: false, }; this.id = this.props.id || getId(); } - onFocus = (ev) => { + public focus() { + this.input.focus(); + } + + private onFocus = (ev) => { this.setState({ focused: true, }); @@ -93,7 +128,7 @@ export default class Field extends React.PureComponent { } }; - onChange = (ev) => { + private onChange = (ev) => { this.validateOnChange(); // Parent component may have supplied its own `onChange` as well if (this.props.onChange) { @@ -101,7 +136,7 @@ export default class Field extends React.PureComponent { } }; - onBlur = (ev) => { + private onBlur = (ev) => { this.setState({ focused: false, }); @@ -114,11 +149,7 @@ export default class Field extends React.PureComponent { } }; - focus() { - this.input.focus(); - } - - async validate({ focused, allowEmpty = true }) { + private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) { if (!this.props.onValidate) { return; } @@ -149,56 +180,42 @@ export default class Field extends React.PureComponent { } } - /* - * This was changed from throttle to debounce: this is more traditional for - * form validation since it means that the validation doesn't happen at all - * until the user stops typing for a bit (debounce defaults to not running on - * the leading edge). If we're doing an HTTP hit on each validation, we have more - * incentive to prevent validating input that's very unlikely to be valid. - * We may find that we actually want different behaviour for registration - * fields, in which case we can add some options to control it. - */ - validateOnChange = debounce(() => { - this.validate({ - focused: true, - }); - }, VALIDATION_THROTTLE_MS); - render() { + + public render() { const { - element, prefix, postfix, className, onValidate, children, + element, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props; - const inputElement = element || "input"; - // Set some defaults for the element - inputProps.type = inputProps.type || "text"; - inputProps.ref = input => this.input = input; + const ref = input => this.input = input; inputProps.placeholder = inputProps.placeholder || inputProps.label; inputProps.id = this.id; // this overwrites the id from props inputProps.onFocus = this.onFocus; inputProps.onChange = this.onChange; inputProps.onBlur = this.onBlur; - inputProps.list = list; - const fieldInput = React.createElement(inputElement, inputProps, children); + // Appease typescript's inference + const inputProps_ = {...inputProps, ref, list}; + + const fieldInput = React.createElement(this.props.element, inputProps_, children); let prefixContainer = null; - if (prefix) { - prefixContainer = {prefix}; + if (prefixComponent) { + prefixContainer = {prefixComponent}; } let postfixContainer = null; - if (postfix) { - postfixContainer = {postfix}; + if (postfixComponent) { + postfixContainer = {postfixComponent}; } const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined; - const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, { + const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do // properly. - mx_Field_labelAlwaysTopLeft: prefix, + mx_Field_labelAlwaysTopLeft: prefixComponent, mx_Field_valid: onValidate && this.state.valid === true, mx_Field_invalid: hasValidationFlag ? flagInvalid diff --git a/src/components/views/elements/GroupsButton.js b/src/components/views/elements/GroupsButton.js deleted file mode 100644 index dd1118aba0..0000000000 --- a/src/components/views/elements/GroupsButton.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -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 * as sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; - -const GroupsButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -GroupsButton.propTypes = { - size: PropTypes.string, -}; - -export default GroupsButton; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index edadc841a3..e39075cedc 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -27,6 +25,7 @@ import AccessibleButton from "./AccessibleButton"; import Modal from "../../../Modal"; import * as sdk from "../../../index"; import {Key} from "../../../Keyboard"; +import FocusLock from "react-focus-lock"; export default class ImageView extends React.Component { static propTypes = { @@ -50,16 +49,6 @@ export default class ImageView extends React.Component { this.state = { rotationDegrees: 0 }; } - // XXX: keyboard shortcuts for managing dialogs should be done by the modal - // dialog base class somehow, surely... - componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } - onKeyDown = (ev) => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); @@ -195,7 +184,14 @@ export default class ImageView extends React.Component { const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style}; return ( -
    +
    @@ -231,7 +227,7 @@ export default class ImageView extends React.Component {
    -
    + ); } } diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 06f025f236..7f9bfdebf4 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -156,70 +156,16 @@ export default class PersistedElement extends React.Component { child.style.display = visible ? 'block' : 'none'; } - /* - * Clip element bounding rectangle to that of the parent elements. - * This is not a full visibility check, but prevents the persisted - * element from overflowing parent containers when inside a scrolled - * area. - */ - _getClippedBoundingClientRect(element) { - let parentElement = element.parentElement; - let rect = element.getBoundingClientRect(); - - rect = new DOMRect(rect.left, rect.top, rect.width, rect.height); - - while (parentElement) { - const parentRect = parentElement.getBoundingClientRect(); - - if (parentRect.left > rect.left) { - rect.width = rect.width - (parentRect.left - rect.left); - rect.x = parentRect.x; - } - - if (parentRect.top > rect.top) { - rect.height = rect.height - (parentRect.top - rect.top); - rect.y = parentRect.y; - } - - if (parentRect.right < rect.right) { - rect.width = rect.width - (rect.right - parentRect.right); - } - - if (parentRect.bottom < rect.bottom) { - rect.height = rect.height - (rect.bottom - parentRect.bottom); - } - - parentElement = parentElement.parentElement; - } - - if (rect.width < 0) rect.width = 0; - if (rect.height < 0) rect.height = 0; - - return rect; - } - updateChildPosition(child, parent) { if (!child || !parent) return; const parentRect = parent.getBoundingClientRect(); - const clipRect = this._getClippedBoundingClientRect(parent); - - Object.assign(child.parentElement.style, { - position: 'absolute', - top: clipRect.top + 'px', - left: clipRect.left + 'px', - width: clipRect.width + 'px', - height: clipRect.height + 'px', - overflow: "hidden", - }); - Object.assign(child.style, { position: 'absolute', - top: (parentRect.top - clipRect.top) + 'px', - left: (parentRect.left - clipRect.left) + 'px', + top: parentRect.top + 'px', + left: parentRect.left + 'px', width: parentRect.width + 'px', height: parentRect.height + 'px', - overflow: "hidden", }); } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 917ab70626..f237521839 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; +import {Action} from "../../../dispatcher/actions"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -290,7 +291,7 @@ export default class ReplyThread extends React.Component { events, }, this.loadNextEvent); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } getReplyThreadColorClass(ev) { diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.js index d3de6a5d34..04bbe1c3de 100644 --- a/src/components/views/elements/RoomAliasField.js +++ b/src/components/views/elements/RoomAliasField.js @@ -45,10 +45,10 @@ export default class RoomAliasField extends React.PureComponent { const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : return ( this._fieldRef = ref} onValidate={this._onValidate} placeholder={_t("e.g. my-room")} @@ -87,7 +87,7 @@ export default class RoomAliasField extends React.PureComponent { }, { key: "required", test: async ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t("Please provide a room alias"), + invalid: () => _t("Please provide a room address"), }, { key: "taken", final: true, @@ -107,8 +107,8 @@ export default class RoomAliasField extends React.PureComponent { return !!err.errcode; } }, - valid: () => _t("This alias is available to use"), - invalid: () => _t("This alias is already in use"), + valid: () => _t("This address is available to use"), + invalid: () => _t("This address is already in use"), }, ], }); diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx new file mode 100644 index 0000000000..341f59d5da --- /dev/null +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -0,0 +1,56 @@ +/* +Copyright 2020 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 { randomString } from "matrix-js-sdk/src/randomstring"; + +const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg"); + +interface IProps extends React.InputHTMLAttributes { +} + +interface IState { +} + +export default class StyledCheckbox extends React.PureComponent { + private id: string; + + public static readonly defaultProps = { + className: "", + } + + constructor(props: IProps) { + super(props); + // 56^10 so unlikely chance of collision. + this.id = "checkbox_" + randomString(10); + } + + public render() { + const { children, className, ...otherProps } = this.props; + return + + + + } +} \ No newline at end of file diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.tsx similarity index 69% rename from src/components/views/elements/Tooltip.js rename to src/components/views/elements/Tooltip.tsx index 4807ade3db..38960d1a58 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.tsx @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -18,67 +18,68 @@ limitations under the License. */ -import React from 'react'; +import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; +import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload'; +import { Action } from '../../../dispatcher/actions'; const MIN_TOOLTIP_HEIGHT = 25; -export default createReactClass({ - displayName: 'Tooltip', - - propTypes: { +interface IProps { // Class applied to the element used to position the tooltip - className: PropTypes.string, + className: string, // Class applied to the tooltip itself - tooltipClassName: PropTypes.string, + tooltipClassName?: string, // Whether the tooltip is visible or hidden. // The hidden state allows animating the tooltip away via CSS. // Defaults to visible if unset. - visible: PropTypes.bool, + visible?: boolean, // the react element to put into the tooltip - label: PropTypes.node, - }, + label: React.ReactNode, +} - getDefaultProps() { - return { - visible: true, - }; - }, +export default class Tooltip extends React.Component { + private tooltipContainer: HTMLElement; + private tooltip: void | Element | Component; + private parent: Element; + + + public static readonly defaultProps = { + visible: true, + }; // Create a wrapper for the tooltip outside the parent and attach it to the body element - componentDidMount: function() { + public componentDidMount() { this.tooltipContainer = document.createElement("div"); this.tooltipContainer.className = "mx_Tooltip_wrapper"; document.body.appendChild(this.tooltipContainer); - window.addEventListener('scroll', this._renderTooltip, true); + window.addEventListener('scroll', this.renderTooltip, true); - this.parent = ReactDOM.findDOMNode(this).parentNode; + this.parent = ReactDOM.findDOMNode(this).parentNode as Element; - this._renderTooltip(); - }, + this.renderTooltip(); + } - componentDidUpdate: function() { - this._renderTooltip(); - }, + public componentDidUpdate() { + this.renderTooltip(); + } // Remove the wrapper element, as the tooltip has finished using it - componentWillUnmount: function() { - dis.dispatch({ - action: 'view_tooltip', + public componentWillUnmount() { + dis.dispatch({ + action: Action.ViewTooltip, tooltip: null, parent: null, }); ReactDOM.unmountComponentAtNode(this.tooltipContainer); document.body.removeChild(this.tooltipContainer); - window.removeEventListener('scroll', this._renderTooltip, true); - }, + window.removeEventListener('scroll', this.renderTooltip, true); + } - _updatePosition(style) { + private updatePosition(style: {[key: string]: any}) { const parentBox = this.parent.getBoundingClientRect(); let offset = 0; if (parentBox.height > MIN_TOOLTIP_HEIGHT) { @@ -91,16 +92,15 @@ export default createReactClass({ style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.left = 6 + parentBox.right + window.pageXOffset; return style; - }, + } - _renderTooltip: function() { + private renderTooltip = () => { // Add the parent's position to the tooltips, so it's correctly // positioned, also taking into account any window zoom // NOTE: The additional 6 pixels for the left position, is to take account of the // tooltips chevron - const parent = ReactDOM.findDOMNode(this).parentNode; - let style = {}; - style = this._updatePosition(style); + const parent = ReactDOM.findDOMNode(this).parentNode as Element; + const 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"; @@ -118,21 +118,21 @@ export default createReactClass({ ); // Render the tooltip manually, as we wish it not to be rendered within the parent - this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer); + this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer); // Tell the roomlist about us so it can manipulate us if it wishes - dis.dispatch({ - action: 'view_tooltip', + dis.dispatch({ + action: Action.ViewTooltip, tooltip: this.tooltip, parent: parent, }); - }, + } - render: function() { + public render() { // Render a placeholder return (
    ); - }, -}); + } +} diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index 3c4352105e..eb3f83dcdf 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -67,7 +67,13 @@ class Category extends React.PureComponent { const localScrollTop = Math.max(0, scrollTop - listTop); return ( -
    +

    {name}

    diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 75f23c5761..36aa4ff782 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import {MenuItem} from "../../structures/ContextMenu"; class Emoji extends React.PureComponent { static propTypes = { @@ -30,14 +31,18 @@ class Emoji extends React.PureComponent { const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); return ( -
  • onClick(emoji)} + onClick(emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className="mx_EmojiPicker_item_wrapper"> + className="mx_EmojiPicker_item_wrapper" + label={emoji.unicode} + >
    {emoji.unicode}
    -
  • + ); } } diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index cacc15a5f9..16a0fc67e7 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -147,8 +147,12 @@ class EmojiPicker extends React.Component { // We update this here instead of through React to avoid re-render on scroll. if (cat.visible) { cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible"); + cat.ref.current.setAttribute("aria-selected", true); + cat.ref.current.setAttribute("tabindex", 0); } else { cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); + cat.ref.current.setAttribute("aria-selected", false); + cat.ref.current.setAttribute("tabindex", -1); } } } diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index b98e90e9b1..c53437e02d 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -16,23 +16,89 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import classNames from "classnames"; + +import {_t} from "../../../languageHandler"; +import {Key} from "../../../Keyboard"; class Header extends React.PureComponent { static propTypes = { categories: PropTypes.arrayOf(PropTypes.object).isRequired, onAnchorClick: PropTypes.func.isRequired, - refs: PropTypes.object, + }; + + findNearestEnabled(index, delta) { + index += this.props.categories.length; + const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories]; + + while (index < cats.length && index >= 0) { + if (cats[index].enabled) return index % this.props.categories.length; + index += delta > 0 ? 1 : -1; + } + } + + changeCategoryRelative(delta) { + const current = this.props.categories.findIndex(c => c.visible); + this.changeCategoryAbsolute(current + delta, delta); + } + + changeCategoryAbsolute(index, delta=1) { + const category = this.props.categories[this.findNearestEnabled(index, delta)]; + if (category) { + this.props.onAnchorClick(category.id); + category.ref.current.focus(); + } + } + + // Implements ARIA Tabs with Automatic Activation pattern + // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html + onKeyDown = (ev) => { + let handled = true; + switch (ev.key) { + case Key.ARROW_LEFT: + this.changeCategoryRelative(-1); + break; + case Key.ARROW_RIGHT: + this.changeCategoryRelative(1); + break; + + case Key.HOME: + this.changeCategoryAbsolute(0); + break; + case Key.END: + this.changeCategoryAbsolute(this.props.categories.length - 1, -1); + break; + default: + handled = false; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } }; render() { return ( - ); } diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 0bc799d356..2f30ae767e 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -27,8 +27,7 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀 if (!data) { throw new Error(`Emoji ${emoji} doesn't exist in emojibase`); } - // Prefer our unicode value for quick reactions as we sometimes use variation selectors. - return Object.assign({}, data, { unicode: emoji }); + return data; }); class QuickReactions extends React.Component { @@ -72,7 +71,7 @@ class QuickReactions extends React.Component { } -
      +
        {QUICK_REACTIONS.map(emoji => - -
        - { this.props.policyUrl ? _t( - "Please help improve Riot.im by sending anonymous usage data. " + - "This will use a cookie " + - "(please see our Cookie Policy).", - {}, - { - 'UsageDataLink': (sub) => - { sub } - , - // XXX: We need to link to the page that explains our cookies - 'PolicyLink': (sub) => - { sub } - - , - }, - ) : _t( - "Please help improve Riot.im by sending anonymous usage data. " + - "This will use a cookie.", - {}, - { - 'UsageDataLink': (sub) => - { sub } - , - }, - ) } -
        - - { _t("Yes, I want to help!") } - - - {_t('Close')} - -
    - ); - } -} diff --git a/src/components/views/globals/MatrixToolbar.js b/src/components/views/globals/MatrixToolbar.js deleted file mode 100644 index 758e4d62aa..0000000000 --- a/src/components/views/globals/MatrixToolbar.js +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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 createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; -import Notifier from '../../../Notifier'; -import AccessibleButton from '../../../components/views/elements/AccessibleButton'; - -export default createReactClass({ - displayName: 'MatrixToolbar', - - hideToolbar: function() { - Notifier.setToolbarHidden(true); - }, - - onClick: function() { - Notifier.setEnabled(true); - }, - - render: function() { - return ( -
    - -
    - { _t('You are not receiving desktop notifications') } { _t('Enable them now') } -
    - {_t('Close')} -
    - ); - }, -}); diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js deleted file mode 100644 index dedccdc6b6..0000000000 --- a/src/components/views/globals/NewVersionBar.js +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import * as sdk from '../../../index'; -import Modal from '../../../Modal'; -import PlatformPeg from '../../../PlatformPeg'; -import { _t } from '../../../languageHandler'; - -/** - * Check a version string is compatible with the Changelog - * dialog ([vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]) - */ -function checkVersion(ver) { - const parts = ver.split('-'); - return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js'; -} - -export default createReactClass({ - propTypes: { - version: PropTypes.string.isRequired, - newVersion: PropTypes.string.isRequired, - releaseNotes: PropTypes.string, - }, - - displayReleaseNotes: function(releaseNotes) { - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - Modal.createTrackedDialog('Display release notes', '', QuestionDialog, { - title: _t("What's New"), - description:
    {releaseNotes}
    , - button: _t("Update"), - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get().installUpdate(); - } - }, - }); - }, - - displayChangelog: function() { - const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog'); - Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, { - version: this.props.version, - newVersion: this.props.newVersion, - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get().installUpdate(); - } - }, - }); - }, - - onUpdateClicked: function() { - PlatformPeg.get().installUpdate(); - }, - - render: function() { - let action_button; - // If we have release notes to display, we display them. Otherwise, - // we display the Changelog Dialog which takes two versions and - // automatically tells you what's changed (provided the versions - // are in the right format) - if (this.props.releaseNotes) { - action_button = ( - - ); - } else if (checkVersion(this.props.version) && checkVersion(this.props.newVersion)) { - action_button = ( - - ); - } else if (PlatformPeg.get()) { - action_button = ( - - ); - } - return ( -
    - -
    - {_t("A new version of Riot is available.")} -
    - {action_button} -
    - ); - }, -}); diff --git a/src/components/views/globals/PasswordNagBar.js b/src/components/views/globals/PasswordNagBar.js deleted file mode 100644 index 74735ca5ea..0000000000 --- a/src/components/views/globals/PasswordNagBar.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd - -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 createReactClass from 'create-react-class'; -import * as sdk from '../../../index'; -import Modal from '../../../Modal'; -import { _t } from '../../../languageHandler'; - -export default createReactClass({ - onUpdateClicked: function() { - const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog'); - Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog); - }, - - render: function() { - const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable"; - return ( -
    - -
    - { _t( - "To return to your account in future you need to set a password", - {}, - { 'u': (sub) => { sub } }, - ) } -
    - -
    - ); - }, -}); diff --git a/src/components/views/globals/ServerLimitBar.js b/src/components/views/globals/ServerLimitBar.js deleted file mode 100644 index 7d414a2826..0000000000 --- a/src/components/views/globals/ServerLimitBar.js +++ /dev/null @@ -1,99 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import classNames from 'classnames'; -import { _td } from '../../../languageHandler'; -import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; - -export default createReactClass({ - propTypes: { - // 'hard' if the logged in user has been locked out, 'soft' if they haven't - kind: PropTypes.string, - adminContact: PropTypes.string, - // The type of limit that has been hit. - limitType: PropTypes.string.isRequired, - }, - - getDefaultProps: function() { - return { - kind: 'hard', - }; - }, - - render: function() { - const toolbarClasses = { - 'mx_MatrixToolbar': true, - }; - - let adminContact; - let limitError; - if (this.props.kind === 'hard') { - toolbarClasses['mx_MatrixToolbar_error'] = true; - - adminContact = messageForResourceLimitError( - this.props.limitType, - this.props.adminContact, - { - '': _td("Please contact your service administrator to continue using the service."), - }, - ); - limitError = messageForResourceLimitError( - this.props.limitType, - this.props.adminContact, - { - 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), - '': _td("This homeserver has exceeded one of its resource limits."), - }, - ); - } else { - toolbarClasses['mx_MatrixToolbar_info'] = true; - adminContact = messageForResourceLimitError( - this.props.limitType, - this.props.adminContact, - { - '': _td("Please contact your service administrator to get this limit increased."), - }, - ); - limitError = messageForResourceLimitError( - this.props.limitType, - this.props.adminContact, - { - 'monthly_active_user': _td( - "This homeserver has hit its Monthly Active User limit so " + - "some users will not be able to log in.", - ), - '': _td( - "This homeserver has exceeded one of its resource limits so " + - "some users will not be able to log in.", - ), - }, - {'b': sub => {sub}}, - ); - } - return ( -
    -
    - {limitError} - {' '} - {adminContact} -
    -
    - ); - }, -}); diff --git a/src/components/views/globals/UpdateCheckBar.js b/src/components/views/globals/UpdateCheckBar.js deleted file mode 100644 index 32b38ff5b0..0000000000 --- a/src/components/views/globals/UpdateCheckBar.js +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2017, 2019 Michael Telatynski <7t3chguy@gmail.com> - -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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; -import PlatformPeg from '../../../PlatformPeg'; -import AccessibleButton from '../../../components/views/elements/AccessibleButton'; - -export default createReactClass({ - propTypes: { - status: PropTypes.string.isRequired, - // Currently for error detail but will be usable for download progress - // once that is a thing that squirrel passes through electron. - detail: PropTypes.string, - }, - - getDefaultProps: function() { - return { - detail: '', - }; - }, - - getStatusText: function() { - // we can't import the enum from riot-web as we don't want matrix-react-sdk - // to depend on riot-web. so we grab it as a normal object via API instead. - const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum(); - switch (this.props.status) { - case updateCheckStatusEnum.ERROR: - return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail }); - case updateCheckStatusEnum.CHECKING: - return _t('Checking for an update...'); - case updateCheckStatusEnum.NOTAVAILABLE: - return _t('No update available.'); - case updateCheckStatusEnum.DOWNLOADING: - return _t('Downloading update...'); - } - }, - - hideToolbar: function() { - PlatformPeg.get().stopUpdateCheck(); - }, - - render: function() { - const message = this.getStatusText(); - const warning = _t('Warning'); - - if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) { - return
    ; - } - - const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum(); - const doneStatuses = [ - updateCheckStatusEnum.ERROR, - updateCheckStatusEnum.NOTAVAILABLE, - ]; - - let image; - if (doneStatuses.includes(this.props.status)) { - image = ; - } else { - image = ; - } - - return ( -
    - {image} -
    - {message} -
    - - {_t('Close')} - -
    - ); - }, -}); diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js deleted file mode 100644 index 2582cab573..0000000000 --- a/src/components/views/groups/GroupMemberInfo.js +++ /dev/null @@ -1,208 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd -Copyright 2019 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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import dis from '../../../dispatcher/dispatcher'; -import Modal from '../../../Modal'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import { GroupMemberType } from '../../../groups'; -import GroupStore from '../../../stores/GroupStore'; -import AccessibleButton from '../elements/AccessibleButton'; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {Action} from "../../../dispatcher/actions"; - -export default createReactClass({ - displayName: 'GroupMemberInfo', - - statics: { - contextType: MatrixClientContext, - }, - - propTypes: { - groupId: PropTypes.string, - groupMember: GroupMemberType, - isInvited: PropTypes.bool, - }, - - getInitialState: function() { - return { - removingUser: false, - isUserPrivilegedInGroup: null, - }; - }, - - componentDidMount: function() { - this._unmounted = false; - this._initGroupStore(this.props.groupId); - }, - - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(newProps) { - if (newProps.groupId !== this.props.groupId) { - this._unregisterGroupStore(this.props.groupId); - this._initGroupStore(newProps.groupId); - } - }, - - componentWillUnmount() { - this._unmounted = true; - this._unregisterGroupStore(this.props.groupId); - }, - - _initGroupStore(groupId) { - GroupStore.registerListener(groupId, this.onGroupStoreUpdated); - }, - - _unregisterGroupStore(groupId) { - GroupStore.unregisterListener(this.onGroupStoreUpdated); - }, - - onGroupStoreUpdated: function() { - if (this._unmounted) return; - this.setState({ - isUserInvited: GroupStore.getGroupInvitedMembers(this.props.groupId).some( - (m) => m.userId === this.props.groupMember.userId, - ), - isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId), - }); - }, - - _onKick: function() { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createDialog(ConfirmUserActionDialog, { - matrixClient: this.context, - groupMember: this.props.groupMember, - action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), - title: this.state.isUserInvited ? _t('Disinvite this user from community?') - : _t('Remove this user from community?'), - danger: true, - onFinished: (proceed) => { - if (!proceed) return; - - this.setState({removingUser: true}); - this.context.removeUserFromGroup( - this.props.groupId, this.props.groupMember.userId, - ).then(() => { - // return to the user list - dis.dispatch({ - action: Action.ViewUser, - member: null, - }); - }).catch((e) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { - title: _t('Error'), - description: this.state.isUserInvited ? - _t('Failed to withdraw invitation') : - _t('Failed to remove user from community'), - }); - }).finally(() => { - this.setState({removingUser: false}); - }); - }, - }); - }, - - _onCancel: function(e) { - // Go back to the user list - dis.dispatch({ - action: Action.ViewUser, - member: null, - }); - }, - - onRoomTileClick(roomId) { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); - }, - - render: function() { - if (this.state.removingUser) { - const Spinner = sdk.getComponent("elements.Spinner"); - return
    - -
    ; - } - - let adminTools; - if (this.state.isUserPrivilegedInGroup) { - const kickButton = ( - - { this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') } - - ); - - // No make/revoke admin API yet - /*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); - giveModButton = - {giveOpLabel} - ;*/ - - if (kickButton) { - adminTools = -
    -

    { _t("Admin Tools") }

    -
    - { kickButton } -
    -
    ; - } - } - - - const avatarUrl = this.props.groupMember.avatarUrl; - let avatarElement; - if (avatarUrl) { - const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800); - avatarElement = (
    - -
    ); - } - - const groupMemberName = ( - this.props.groupMember.displayname || this.props.groupMember.userId - ); - - return ( -
    - - - - - { avatarElement } -

    { groupMemberName }

    - -
    -
    - { this.props.groupMember.userId } -
    -
    - - { adminTools } -
    -
    - ); - }, -}); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 48b9c58cb8..95eb37b588 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -1,7 +1,7 @@ /* Copyright 2019 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -22,11 +22,9 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; -import Modal from '../../../Modal'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext from "../../../contexts/RoomContext"; -import SettingsStore from '../../../settings/SettingsStore'; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -41,18 +39,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo const tile = getTile && getTile(); const replyThread = getReplyThread && getReplyThread(); - const onCryptoClick = () => { - Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', - import('../../../async-components/views/dialogs/EncryptedEventDialog'), - {event: mxEvent}, - ); - }; - - let e2eInfoCallback = null; - if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) { - e2eInfoCallback = onCryptoClick; - } - const buttonRect = button.current.getBoundingClientRect(); contextMenu = ; diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index a7ff7dce96..09824cd315 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; +import dis from "../../../dispatcher/dispatcher"; export default class ReactionsRowButton extends React.PureComponent { static propTypes = { @@ -60,6 +61,7 @@ export default class ReactionsRowButton extends React.PureComponent { "key": content, }, }); + dis.dispatch({action: "message_sent"}); } }; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0392746c94..34136b2177 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -25,7 +25,7 @@ import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import createRoom from '../../../createRoom'; +import createRoom, {privateShouldBeEncrypted} from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; @@ -64,10 +64,6 @@ const _disambiguateDevices = (devices) => { }; export const getE2EStatus = (cli, userId, devices) => { - if (!SettingsStore.getValue("feature_cross_signing")) { - const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); - return hasUnverifiedDevice ? "warning" : "verified"; - } const isMe = userId === cli.getUserId(); const userTrust = cli.checkUserTrust(userId); if (!userTrust.isCrossSigningVerified()) { @@ -112,7 +108,7 @@ async function openDMForUser(matrixClient, userId) { dmUserId: userId, }; - if (SettingsStore.getValue("feature_cross_signing")) { + if (privateShouldBeEncrypted()) { // Check whether all users have uploaded device keys before. // If so, enable encryption in the new room. const usersToDevicesMap = await matrixClient.downloadKeys([userId]); @@ -167,9 +163,7 @@ function DeviceItem({userId, device}) { // cross-signing so that other users can then safely trust you. // For other people's devices, the more general verified check that // includes locally verified devices can be used. - const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ? - deviceTrust.isCrossSigningVerified() : - deviceTrust.isVerified(); + const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); const classes = classNames("mx_UserInfo_device", { mx_UserInfo_device_verified: isVerified, @@ -248,9 +242,7 @@ function DevicesSection({devices, userId, loading}) { // cross-signing so that other users can then safely trust you. // For other people's devices, the more general verified check that // includes locally verified devices can be used. - const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ? - deviceTrust.isCrossSigningVerified() : - deviceTrust.isVerified(); + const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); if (isVerified) { expandSectionDevices.push(device); @@ -1309,8 +1301,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { const userTrust = cli.checkUserTrust(member.userId); const userVerified = userTrust.isCrossSigningVerified(); const isMe = member.userId === cli.getUserId(); - const canVerify = SettingsStore.getValue("feature_cross_signing") && - homeserverSupportsCrossSigning && !userVerified && !isMe; + const canVerify = homeserverSupportsCrossSigning && !userVerified && !isMe; const setUpdating = (updating) => { setPendingUpdateCount(count => count + (updating ? 1 : -1)); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 3994d78390..37d1e66e98 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -220,10 +220,10 @@ export default class AliasSettings extends React.Component { } }).catch((err) => { console.error(err); - Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, { - title: _t("Error creating alias"), + Modal.createTrackedDialog('Error creating address', '', ErrorDialog, { + title: _t("Error creating address"), description: _t( - "There was an error creating that alias. It may not be allowed by the server " + + "There was an error creating that address. It may not be allowed by the server " + "or a temporary failure occurred.", ), }); @@ -245,15 +245,15 @@ export default class AliasSettings extends React.Component { console.error(err); let description; if (err.errcode === "M_FORBIDDEN") { - description = _t("You don't have permission to delete the alias."); + description = _t("You don't have permission to delete the address."); } else { description = _t( - "There was an error removing that alias. It may no longer exist or a temporary " + + "There was an error removing that address. It may no longer exist or a temporary " + "error occurred.", ); } - Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, { - title: _t("Error removing alias"), + Modal.createTrackedDialog('Error removing address', '', ErrorDialog, { + title: _t("Error removing address"), description, }); }); diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 975c8e84a5..f5cf1a981c 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -15,8 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; +import React, {createRef} from 'react'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter'; @@ -54,7 +53,7 @@ export default class Autocomplete extends React.PureComponent { autocompleter: Autocompleter; queryRequested: string; debounceCompletionsRequest: NodeJS.Timeout; - containerRef: React.RefObject; + private containerRef = createRef(); constructor(props) { super(props); @@ -78,8 +77,6 @@ export default class Autocomplete extends React.PureComponent { forceComplete: false, }; - - this.containerRef = React.createRef(); } componentDidMount() { @@ -256,14 +253,15 @@ export default class Autocomplete extends React.PureComponent { componentDidUpdate(prevProps: IProps) { this.applyNewProps(prevProps.query, prevProps.room); // this is the selected completion, so scroll it into view if needed - const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`]; - if (selectedCompletion && this.containerRef.current) { - const domNode = ReactDOM.findDOMNode(selectedCompletion); - const offsetTop = domNode && (domNode as HTMLElement).offsetTop; - if (offsetTop > this.containerRef.current.scrollTop + this.containerRef.current.offsetHeight || - offsetTop < this.containerRef.current.scrollTop) { - this.containerRef.current.scrollTop = offsetTop - this.containerRef.current.offsetTop; - } + const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`] as HTMLElement; + + if (selectedCompletion) { + selectedCompletion.scrollIntoView({ + behavior: "auto", + block: "nearest", + }); + } else if (this.containerRef.current) { + this.containerRef.current.scrollTo({ top: 0 }); } } diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 1ac68e4b12..d6a3b156d5 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -141,15 +141,6 @@ export default createReactClass({ return counters; }, - _onScroll: function(rect) { - if (this.props.onResize) { - this.props.onResize(); - } - - /* Force refresh of PersistedElements which may be partially hidden */ - window.dispatchEvent(new Event('resize')); - }, - render: function() { const CallView = sdk.getComponent("voip.CallView"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -274,7 +265,7 @@ export default createReactClass({ } return ( - + { stateViews } { appsDrawer } { fileDropTarget } diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 2e4a966404..82f61e0e1f 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -74,6 +74,7 @@ function selectionEquals(a: Selection, b: Selection): boolean { export default class BasicMessageEditor extends React.Component { static propTypes = { onChange: PropTypes.func, + onPaste: PropTypes.func, // returns true if handled and should skip internal onPaste handler model: PropTypes.instanceOf(EditorModel).isRequired, room: PropTypes.instanceOf(Room).isRequired, placeholder: PropTypes.string, @@ -254,6 +255,12 @@ export default class BasicMessageEditor extends React.Component { } _onPaste = (event) => { + event.preventDefault(); // we always handle the paste ourselves + if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { + // to prevent double handling, allow props.onPaste to skip internal onPaste + return true; + } + const {model} = this.props; const {partCreator} = model; const partsText = event.clipboardData.getData("application/x-riot-composer"); @@ -269,7 +276,6 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = true; const range = getRangeForSelection(this._editorRef, model, document.getSelection()); replaceRangeAndMoveCaret(range, parts); - event.preventDefault(); } _onInput = (event) => { @@ -353,6 +359,8 @@ export default class BasicMessageEditor extends React.Component { } _onSelectionChange = () => { + const {isEmpty} = this.props.model; + this._refreshLastCaretIfNeeded(); const selection = document.getSelection(); if (this._hasTextSelected && selection.isCollapsed) { @@ -360,7 +368,7 @@ export default class BasicMessageEditor extends React.Component { if (this._formatBarRef) { this._formatBarRef.hide(); } - } else if (!selection.isCollapsed) { + } else if (!selection.isCollapsed && !isEmpty) { this._hasTextSelected = true; if (this._formatBarRef) { const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); @@ -503,10 +511,6 @@ export default class BasicMessageEditor extends React.Component { } } - getEditableRootNode() { - return this._editorRef; - } - isModified() { return this._modifiedFlag; } diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 5e74656920..bf65c7fb7c 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -20,7 +20,6 @@ import PropTypes from "prop-types"; import classNames from 'classnames'; import {_t, _td} from '../../../languageHandler'; -import {useSettingValue} from "../../../hooks/useSettings"; import AccessibleButton from "../elements/AccessibleButton"; import Tooltip from "../elements/Tooltip"; @@ -42,15 +41,6 @@ const crossSigningRoomTitles = { [E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"), }; -const legacyUserTitles = { - [E2E_STATE.WARNING]: _td("Some sessions for this user are not trusted"), - [E2E_STATE.VERIFIED]: _td("All sessions for this user are trusted"), -}; -const legacyRoomTitles = { - [E2E_STATE.WARNING]: _td("Some sessions in this encrypted room are not trusted"), - [E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"), -}; - const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => { const [hover, setHover] = useState(false); @@ -62,15 +52,10 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => { }, className); let e2eTitle; - const crossSigning = useSettingValue("feature_cross_signing"); - if (crossSigning && isUser) { + if (isUser) { e2eTitle = crossSigningUserTitles[status]; - } else if (crossSigning && !isUser) { + } else { e2eTitle = crossSigningRoomTitles[status]; - } else if (!crossSigning && isUser) { - e2eTitle = legacyUserTitles[status]; - } else if (!crossSigning && !isUser) { - e2eTitle = legacyRoomTitles[status]; } let style; diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 88ed76f118..78c7de887d 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -31,6 +31,7 @@ import {EventStatus} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {Action} from "../../../dispatcher/actions"; function _isReply(mxEvent) { const relatesTo = mxEvent.getContent()["m.relates_to"]; @@ -157,7 +158,7 @@ export default class EditMessageComposer extends React.Component { dis.dispatch({action: 'edit_event', event: nextEvent}); } else { dis.dispatch({action: 'edit_event', event: null}); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } event.preventDefault(); } @@ -165,7 +166,7 @@ export default class EditMessageComposer extends React.Component { _cancelEdit = () => { dis.dispatch({action: "edit_event", event: null}); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } _isContentModified(newContent) { @@ -190,11 +191,12 @@ export default class EditMessageComposer extends React.Component { const roomId = editedEvent.getRoomId(); this._cancelPreviousPendingEdit(); this.context.sendMessage(roomId, editContent); + dis.dispatch({action: "message_sent"}); } // close the event editing and focus composer dis.dispatch({action: "edit_event", event: null}); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }; _cancelPreviousPendingEdit() { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 31797cffb4..7508cf3372 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -104,7 +104,7 @@ export function getHandlerTile(ev) { // fall back to showing hidden events, if we're viewing hidden events // XXX: This is extremely a hack. Possibly these components should have an interface for // declining to render? - if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) { + if (type === "m.key.verification.cancel" || type === "m.key.verification.done") { const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion"); if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) { return; @@ -325,15 +325,6 @@ export default createReactClass({ return; } - // If cross-signing is off, the old behaviour is to scream at the user - // as if they've done something wrong, which they haven't - if (!SettingsStore.getValue("feature_cross_signing")) { - this.setState({ - verified: E2E_STATE.WARNING, - }, this.props.onHeightChanged); - return; - } - if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) { this.setState({ verified: E2E_STATE.NORMAL, @@ -403,7 +394,7 @@ export default createReactClass({ }, shouldHighlight: function() { - const actions = this.context.getPushActionsForEvent(this.props.mxEvent); + const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients @@ -802,6 +793,8 @@ export default createReactClass({ const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null; const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null; + const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); + const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); switch (this.props.tileShape) { case 'notif': { @@ -873,9 +866,10 @@ export default createReactClass({ { ircTimestamp } { avatar } { sender } + { ircPadlock }
    { groupTimestamp } - { !isBubbleMessage && this._renderE2EPadlock() } + { groupPadlock } { thread } { sender } + { ircPadlock }
    { groupTimestamp } - { !isBubbleMessage && this._renderE2EPadlock() } + { groupPadlock } { thread } ); - const deviceName = (this.props.device.ambiguous || this.props.showDeviceId) ? - (this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" : - this.props.device.getDisplayName(); - - // add the deviceId as a titletext to help with debugging - return ( -
    - { indicator } -
    -
    - { deviceName } -
    -
    - -
    - ); - } -} - -MemberDeviceInfo.displayName = 'MemberDeviceInfo'; -MemberDeviceInfo.propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, -}; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js deleted file mode 100644 index ed6c4ad748..0000000000 --- a/src/components/views/rooms/MemberInfo.js +++ /dev/null @@ -1,1165 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 Vector Creations Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -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. -*/ - -/* - * State vars: - * 'can': { - * kick: boolean, - * ban: boolean, - * mute: boolean, - * modifyLevel: boolean - * }, - * 'muted': boolean, - * 'isTargetMod': boolean - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import classNames from 'classnames'; -import dis from '../../../dispatcher/dispatcher'; -import Modal from '../../../Modal'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import createRoom from '../../../createRoom'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import * as Unread from '../../../Unread'; -import { findReadReceiptFromUserId } from '../../../utils/Receipt'; -import AccessibleButton from '../elements/AccessibleButton'; -import RoomViewStore from '../../../stores/RoomViewStore'; -import SdkConfig from '../../../SdkConfig'; -import MultiInviter from "../../../utils/MultiInviter"; -import SettingsStore from "../../../settings/SettingsStore"; -import E2EIcon from "./E2EIcon"; -import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {Action} from "../../../dispatcher/actions"; - -export default createReactClass({ - displayName: 'MemberInfo', - - propTypes: { - member: PropTypes.object.isRequired, - }, - - getInitialState: function() { - return { - can: { - kick: false, - ban: false, - mute: false, - modifyLevel: false, - synapseDeactivate: false, - redactMessages: false, - }, - muted: false, - isTargetMod: false, - updating: 0, - devicesLoading: true, - devices: null, - isIgnoring: false, - }; - }, - - statics: { - contextType: MatrixClientContext, - }, - - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { - this._cancelDeviceList = null; - const cli = this.context; - - // only display the devices list if our client supports E2E - this._enableDevices = cli.isCryptoEnabled(); - - cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); - cli.on("Room", this.onRoom); - cli.on("deleteRoom", this.onDeleteRoom); - cli.on("Room.timeline", this.onRoomTimeline); - cli.on("Room.name", this.onRoomName); - cli.on("Room.receipt", this.onRoomReceipt); - cli.on("RoomState.events", this.onRoomStateEvents); - cli.on("RoomMember.name", this.onRoomMemberName); - cli.on("RoomMember.membership", this.onRoomMemberMembership); - cli.on("accountData", this.onAccountData); - - this._checkIgnoreState(); - - this._updateStateForNewMember(this.props.member); - }, - - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { - if (this.props.member.userId !== newProps.member.userId) { - this._updateStateForNewMember(newProps.member); - } - }, - - componentWillUnmount: function() { - const client = this.context; - if (client) { - client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - client.removeListener("Room", this.onRoom); - client.removeListener("deleteRoom", this.onDeleteRoom); - client.removeListener("Room.timeline", this.onRoomTimeline); - client.removeListener("Room.name", this.onRoomName); - client.removeListener("Room.receipt", this.onRoomReceipt); - client.removeListener("RoomState.events", this.onRoomStateEvents); - client.removeListener("RoomMember.name", this.onRoomMemberName); - client.removeListener("RoomMember.membership", this.onRoomMemberMembership); - client.removeListener("accountData", this.onAccountData); - } - if (this._cancelDeviceList) { - this._cancelDeviceList(); - } - }, - - _checkIgnoreState: function() { - const isIgnoring = this.context.isUserIgnored(this.props.member.userId); - this.setState({isIgnoring: isIgnoring}); - }, - - _disambiguateDevices: function(devices) { - const names = Object.create(null); - for (let i = 0; i < devices.length; i++) { - const name = devices[i].getDisplayName(); - const indexList = names[name] || []; - indexList.push(i); - names[name] = indexList; - } - for (const name in names) { - if (names[name].length > 1) { - names[name].forEach((j)=>{ - devices[j].ambiguous = true; - }); - } - } - }, - - onDeviceVerificationChanged: function(userId, device) { - if (!this._enableDevices) { - return; - } - - if (userId === this.props.member.userId) { - // no need to re-download the whole thing; just update our copy of - // the list. - - const devices = this.context.getStoredDevicesForUser(userId); - this.setState({ - devices: devices, - e2eStatus: this._getE2EStatus(devices), - }); - } - }, - - _getE2EStatus: function(devices) { - const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); - return hasUnverifiedDevice ? "warning" : "verified"; - }, - - onRoom: function(room) { - this.forceUpdate(); - }, - - onDeleteRoom: function(roomId) { - this.forceUpdate(); - }, - - onRoomTimeline: function(ev, room, toStartOfTimeline) { - if (toStartOfTimeline) return; - this.forceUpdate(); - }, - - onRoomName: function(room) { - this.forceUpdate(); - }, - - onRoomReceipt: function(receiptEvent, room) { - // because if we read a notification, it will affect notification count - // only bother updating if there's a receipt from us - if (findReadReceiptFromUserId(receiptEvent, this.context.credentials.userId)) { - this.forceUpdate(); - } - }, - - onRoomStateEvents: function(ev, state) { - this.forceUpdate(); - }, - - onRoomMemberName: function(ev, member) { - this.forceUpdate(); - }, - - onRoomMemberMembership: function(ev, member) { - if (this.props.member.userId === member.userId) this.forceUpdate(); - }, - - onAccountData: function(ev) { - if (ev.getType() === 'm.direct') { - this.forceUpdate(); - } - }, - - _updateStateForNewMember: async function(member) { - const newState = await this._calculateOpsPermissions(member); - newState.devicesLoading = true; - newState.devices = null; - this.setState(newState); - - if (this._cancelDeviceList) { - this._cancelDeviceList(); - this._cancelDeviceList = null; - } - - this._downloadDeviceList(member); - }, - - _downloadDeviceList: function(member) { - if (!this._enableDevices) { - return; - } - - let cancelled = false; - this._cancelDeviceList = function() { cancelled = true; }; - - const client = this.context; - const self = this; - client.downloadKeys([member.userId], true).then(() => { - return client.getStoredDevicesForUser(member.userId); - }).finally(function() { - self._cancelDeviceList = null; - }).then(function(devices) { - if (cancelled) { - // we got cancelled - presumably a different user now - return; - } - - self._disambiguateDevices(devices); - self.setState({ - devicesLoading: false, - devices: devices, - e2eStatus: self._getE2EStatus(devices), - }); - }, function(err) { - console.log("Error downloading sessions", err); - self.setState({devicesLoading: false}); - }); - }, - - onIgnoreToggle: function() { - const ignoredUsers = this.context.getIgnoredUsers(); - if (this.state.isIgnoring) { - const index = ignoredUsers.indexOf(this.props.member.userId); - if (index !== -1) ignoredUsers.splice(index, 1); - } else { - ignoredUsers.push(this.props.member.userId); - } - - this.context.setIgnoredUsers(ignoredUsers).then(() => { - return this.setState({isIgnoring: !this.state.isIgnoring}); - }); - }, - - onKick: function() { - const membership = this.props.member.membership; - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { - member: this.props.member, - action: membership === "invite" ? _t("Disinvite") : _t("Kick"), - title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), - askReason: membership === "join", - danger: true, - onFinished: (proceed, reason) => { - if (!proceed) return; - - this.setState({ updating: this.state.updating + 1 }); - this.context.kick( - this.props.member.roomId, this.props.member.userId, - reason || undefined, - ).then(function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Kick success"); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Kick error: " + err); - Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { - title: _t("Failed to kick"), - description: ((err && err.message) ? err.message : "Operation failed"), - }); - }, - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); - }); - }, - }); - }, - - onBanOrUnban: function() { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { - member: this.props.member, - action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"), - title: this.props.member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), - askReason: this.props.member.membership !== 'ban', - danger: this.props.member.membership !== 'ban', - onFinished: (proceed, reason) => { - if (!proceed) return; - - this.setState({ updating: this.state.updating + 1 }); - let promise; - if (this.props.member.membership === 'ban') { - promise = this.context.unban( - this.props.member.roomId, this.props.member.userId, - ); - } else { - promise = this.context.ban( - this.props.member.roomId, this.props.member.userId, - reason || undefined, - ); - } - promise.then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Ban success"); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Ban error: " + err); - Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to ban user"), - }); - }, - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); - }); - }, - }); - }, - - onRedactAllMessages: async function() { - const {roomId, userId} = this.props.member; - const room = this.context.getRoom(roomId); - if (!room) { - return; - } - const timelineSet = room.getUnfilteredTimelineSet(); - let eventsToRedact = []; - for (const timeline of timelineSet.getTimelines()) { - eventsToRedact = timeline.getEvents().reduce((events, event) => { - if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) { - return events.concat(event); - } else { - return events; - } - }, eventsToRedact); - } - - const count = eventsToRedact.length; - const user = this.props.member.name; - - if (count === 0) { - const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); - Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { - title: _t("No recent messages by %(user)s found", {user}), - description: -
    -

    { _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

    -
    , - }); - } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const confirmed = await new Promise((resolve) => { - Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { - title: _t("Remove recent messages by %(user)s", {user}), - description: -
    -

    { _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

    -

    { _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

    -
    , - button: _t("Remove %(count)s messages", {count}), - onFinished: resolve, - }); - }); - - if (!confirmed) { - return; - } - - // Submitting a large number of redactions freezes the UI, - // so first yield to allow to rerender after closing the dialog. - await Promise.resolve(); - - console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); - await Promise.all(eventsToRedact.map(async event => { - try { - await this.context.redactEvent(roomId, event.getId()); - } catch (err) { - // log and swallow errors - console.error("Could not redact", event.getId()); - console.error(err); - } - })); - console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); - } - }, - - _warnSelfDemote: function() { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - return new Promise((resolve) => { - Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { - title: _t("Demote yourself?"), - description: -
    - { _t("You will not be able to undo this change as you are demoting yourself, " + - "if you are the last privileged user in the room it will be impossible " + - "to regain privileges.") } -
    , - button: _t("Demote"), - onFinished: resolve, - }); - }); - }, - - onMuteToggle: async function() { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomId = this.props.member.roomId; - const target = this.props.member.userId; - const room = this.context.getRoom(roomId); - if (!room) return; - - // if muting self, warn as it may be irreversible - if (target === this.context.getUserId()) { - try { - if (!(await this._warnSelfDemote())) return; - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - return; - } - } - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - const isMuted = this.state.muted; - const powerLevels = powerLevelEvent.getContent(); - const levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); - let level; - if (isMuted) { // unmute - level = levelToSend; - } else { // mute - level = levelToSend - 1; - } - level = parseInt(level); - - if (!isNaN(level)) { - this.setState({ updating: this.state.updating + 1 }); - this.context.setPowerLevel(roomId, target, level, powerLevelEvent).then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mute toggle success"); - }, function(err) { - console.error("Mute error: " + err); - Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to mute user"), - }); - }, - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); - }); - } - }, - - onModToggle: function() { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomId = this.props.member.roomId; - const target = this.props.member.userId; - const room = this.context.getRoom(roomId); - if (!room) return; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - const me = room.getMember(this.context.credentials.userId); - if (!me) return; - - const defaultLevel = powerLevelEvent.getContent().users_default; - let modLevel = me.powerLevel - 1; - if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults - // toggle the level - const newLevel = this.state.isTargetMod ? defaultLevel : modLevel; - this.setState({ updating: this.state.updating + 1 }); - this.context.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mod toggle success"); - }, function(err) { - if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { - dis.dispatch({action: 'require_registration'}); - } else { - console.error("Toggle moderator error:" + err); - Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to toggle moderator status"), - }); - } - }, - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); - }); - }, - - onSynapseDeactivate: function() { - const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); - Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { - title: _t("Deactivate user?"), - description: -
    { _t( - "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + - "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to " + - "deactivate this user?" - ) }
    , - button: _t("Deactivate user"), - danger: true, - onFinished: (accepted) => { - if (!accepted) return; - this.context.deactivateSynapseUser(this.props.member.userId).catch(e => { - console.error("Failed to deactivate user"); - console.error(e); - - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { - title: _t('Failed to deactivate user'), - description: ((e && e.message) ? e.message : _t("Operation failed")), - }); - }); - }, - }); - }, - - _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { - this.setState({ updating: this.state.updating + 1 }); - this.context.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Power change success"); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to change power level " + err); - Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to change power level"), - }); - }, - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); - }); - }, - - onPowerChange: async function(powerLevel) { - const roomId = this.props.member.roomId; - const target = this.props.member.userId; - const room = this.context.getRoom(roomId); - if (!room) return; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - if (!powerLevelEvent.getContent().users) { - this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; - } - - const myUserId = this.context.getUserId(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - if (myUserId === target) { - try { - if (!(await this._warnSelfDemote())) return; - this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - } - return; - } - - const myPower = powerLevelEvent.getContent().users[myUserId]; - if (parseInt(myPower) === parseInt(powerLevel)) { - Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { - title: _t("Warning!"), - description: -
    - { _t("You will not be able to undo this change as you are promoting the user " + - "to have the same power level as yourself.") }
    - { _t("Are you sure?") } -
    , - button: _t("Continue"), - onFinished: (confirmed) => { - if (confirmed) { - this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } - }, - }); - return; - } - this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - }, - - onNewDMClick: function() { - this.setState({ updating: this.state.updating + 1 }); - createRoom({dmUserId: this.props.member.userId}).finally(() => { - this.setState({ updating: this.state.updating - 1 }); - }); - }, - - onLeaveClick: function() { - dis.dispatch({ - action: 'leave_room', - room_id: this.props.member.roomId, - }); - }, - - _calculateOpsPermissions: async function(member) { - let canDeactivate = false; - if (this.context) { - try { - canDeactivate = await this.context.isSynapseAdministrator(); - } catch (e) { - console.error(e); - } - } - - const defaultPerms = { - can: { - // Calculate permissions for Synapse before doing the PL checks - synapseDeactivate: canDeactivate, - }, - muted: false, - }; - const room = this.context.getRoom(member.roomId); - if (!room) return defaultPerms; - - const powerLevels = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevels) return defaultPerms; - - const me = room.getMember(this.context.credentials.userId); - if (!me) return defaultPerms; - - const them = member; - return { - can: { - ...defaultPerms.can, - ...await this._calculateCanPermissions(me, them, powerLevels.getContent()), - }, - muted: this._isMuted(them, powerLevels.getContent()), - isTargetMod: them.powerLevel > powerLevels.getContent().users_default, - }; - }, - - _calculateCanPermissions: function(me, them, powerLevels) { - const isMe = me.userId === them.userId; - const can = { - kick: false, - ban: false, - mute: false, - modifyLevel: false, - modifyLevelMax: 0, - redactMessages: me.powerLevel >= powerLevels.redact, - }; - - const canAffectUser = them.powerLevel < me.powerLevel || isMe; - if (!canAffectUser) { - //console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); - return can; - } - const editPowerLevel = ( - (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || - powerLevels.state_default - ); - - can.kick = me.powerLevel >= powerLevels.kick; - can.ban = me.powerLevel >= powerLevels.ban; - can.invite = me.powerLevel >= powerLevels.invite; - can.mute = me.powerLevel >= editPowerLevel; - can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel); - can.modifyLevelMax = me.powerLevel; - - return can; - }, - - _isMuted: function(member, powerLevelContent) { - if (!powerLevelContent || !member) return false; - - const levelToSend = ( - (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || - powerLevelContent.events_default - ); - return member.powerLevel < levelToSend; - }, - - onCancel: function(e) { - dis.dispatch({ - action: Action.ViewUser, - member: null, - }); - }, - - onMemberAvatarClick: function() { - const member = this.props.member; - const avatarUrl = member.getMxcAvatarUrl(); - if (!avatarUrl) return; - - const httpUrl = this.context.mxcUrlToHttp(avatarUrl); - const ImageView = sdk.getComponent("elements.ImageView"); - const params = { - src: httpUrl, - name: member.name, - }; - - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, - - onRoomTileClick(roomId) { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); - }, - - _renderDevices: function() { - if (!this._enableDevices) return null; - - const devices = this.state.devices; - const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); - const Spinner = sdk.getComponent("elements.Spinner"); - - let devComponents; - if (this.state.devicesLoading) { - // still loading - devComponents = ; - } else if (devices === null) { - devComponents = _t("Unable to load session list"); - } else if (devices.length === 0) { - devComponents = _t("No sessions with registered encryption keys"); - } else { - devComponents = []; - for (let i = 0; i < devices.length; i++) { - devComponents.push(); - } - } - - return ( -
    -

    { _t("Sessions") }

    -
    - { devComponents } -
    -
    - ); - }, - - onShareUserClick: function() { - const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); - Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { - target: this.props.member, - }); - }, - - _renderUserOptions: function() { - const cli = this.context; - const member = this.props.member; - - let ignoreButton = null; - let insertPillButton = null; - let inviteUserButton = null; - let readReceiptButton = null; - - // Only allow the user to ignore the user if its not ourselves - // same goes for jumping to read receipt - if (member.userId !== cli.getUserId()) { - ignoreButton = ( - - { this.state.isIgnoring ? _t("Unignore") : _t("Ignore") } - - ); - - if (member.roomId) { - const room = cli.getRoom(member.roomId); - const eventId = room.getEventReadUpTo(member.userId); - - const onReadReceiptButton = function() { - dis.dispatch({ - action: 'view_room', - highlighted: true, - event_id: eventId, - room_id: member.roomId, - }); - }; - - const onInsertPillButton = function() { - dis.dispatch({ - action: 'insert_mention', - user_id: member.userId, - }); - }; - - readReceiptButton = ( - - { _t('Jump to read receipt') } - - ); - - insertPillButton = ( - - { _t('Mention') } - - ); - } - - if (this.state.can.invite && (!member || !member.membership || member.membership === 'leave')) { - const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); - const onInviteUserButton = async () => { - try { - // We use a MultiInviter to re-use the invite logic, even though - // we're only inviting one user. - const inviter = new MultiInviter(roomId); - await inviter.invite([member.userId]).then(() => { - if (inviter.getCompletionState(member.userId) !== "invited") - throw new Error(inviter.getErrorText(member.userId)); - }); - } catch (err) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { - title: _t('Failed to invite'), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - } - }; - - inviteUserButton = ( - - { _t('Invite') } - - ); - } - } - - const shareUserButton = ( - - { _t('Share Link to User') } - - ); - - return ( -
    -

    { _t("User Options") }

    -
    - { readReceiptButton } - { shareUserButton } - { insertPillButton } - { ignoreButton } - { inviteUserButton } -
    -
    - ); - }, - - render: function() { - let startChat; - let kickButton; - let banButton; - let muteButton; - let giveModButton; - let redactButton; - let synapseDeactivateButton; - let spinner; - - if (this.props.member.userId !== this.context.credentials.userId) { - // TODO: Immutable DMs replaces a lot of this - const dmRoomMap = new DMRoomMap(this.context); - // dmRooms will not include dmRooms that we have been invited into but did not join. - // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. - // XXX: we potentially want DMs we have been invited to, to also show up here :L - // especially as logic below concerns specially if we haven't joined but have been invited - const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId); - - const RoomTile = sdk.getComponent("rooms.RoomTile"); - - const tiles = []; - for (const roomId of dmRooms) { - const room = this.context.getRoom(roomId); - if (room) { - const myMembership = room.getMyMembership(); - // not a DM room if we have are not joined - if (myMembership !== 'join') continue; - - const them = this.props.member; - // not a DM room if they are not joined - if (!them.membership || them.membership !== 'join') continue; - - const highlight = room.getUnreadNotificationCount('highlight') > 0; - - tiles.push( - , - ); - } - } - - const labelClasses = classNames({ - mx_MemberInfo_createRoom_label: true, - mx_RoomTile_name: true, - }); - let startNewChat = -
    - -
    -
    { _t("Start a chat") }
    -
    ; - - if (tiles.length > 0) startNewChat = null; // Don't offer a button for a new chat if we have one. - - startChat =
    -

    { _t("Direct chats") }

    - { tiles } - { startNewChat } -
    ; - } - - if (this.state.updating) { - const Loader = sdk.getComponent("elements.Spinner"); - spinner = ; - } - - if (this.state.can.kick) { - const membership = this.props.member.membership; - const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); - kickButton = ( - - { kickLabel } - - ); - } - - if (this.state.can.redactMessages) { - redactButton = ( - - { _t("Remove recent messages") } - - ); - } - - if (this.state.can.ban) { - let label = _t("Ban"); - if (this.props.member.membership === 'ban') { - label = _t("Unban"); - } - banButton = ( - - { label } - - ); - } - if (this.state.can.mute) { - const muteLabel = this.state.muted ? _t("Unmute") : _t("Mute"); - muteButton = ( - - { muteLabel } - - ); - } - if (this.state.can.toggleMod) { - const giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); - giveModButton = - { giveOpLabel } - ; - } - - // We don't need a perfect check here, just something to pass as "probably not our homeserver". If - // someone does figure out how to bypass this check the worst that happens is an error. - const sameHomeserver = this.props.member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`); - if (this.state.can.synapseDeactivate && sameHomeserver) { - synapseDeactivateButton = ( - - {_t("Deactivate user")} - - ); - } - - let adminTools; - if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) { - adminTools = -
    -

    { _t("Admin Tools") }

    - -
    - { muteButton } - { kickButton } - { banButton } - { redactButton } - { giveModButton } - { synapseDeactivateButton } -
    -
    ; - } - - const memberName = this.props.member.name; - - let presenceState; - let presenceLastActiveAgo; - let presenceCurrentlyActive; - let statusMessage; - - if (this.props.member.user) { - presenceState = this.props.member.user.presence; - presenceLastActiveAgo = this.props.member.user.lastActiveAgo; - presenceCurrentlyActive = this.props.member.user.currentlyActive; - - if (SettingsStore.isFeatureEnabled("feature_custom_status")) { - statusMessage = this.props.member.user._unstable_statusMessage; - } - } - - const room = this.context.getRoom(this.props.member.roomId); - const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null; - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - - const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; - const hsUrl = this.context.baseUrl; - let showPresence = true; - if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { - showPresence = enablePresenceByHsUrl[hsUrl]; - } - - let presenceLabel = null; - if (showPresence) { - const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); - presenceLabel = ; - } - - let statusLabel = null; - if (statusMessage) { - statusLabel = { statusMessage }; - } - - let roomMemberDetails = null; - let e2eIconElement; - - if (this.props.member.roomId) { // is in room - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - roomMemberDetails =
    -
    - -
    -
    - {presenceLabel} - {statusLabel} -
    -
    ; - - const isEncrypted = this.context.isRoomEncrypted(this.props.member.roomId); - if (this.state.e2eStatus && isEncrypted) { - e2eIconElement = (); - } - } - - const {member} = this.props; - const avatarUrl = member.avatarUrl || (member.getMxcAvatarUrl && member.getMxcAvatarUrl()); - let avatarElement; - if (avatarUrl) { - const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800); - avatarElement =
    - -
    ; - } - - let backButton; - if (this.props.member.roomId) { - backButton = (); - } - - return ( -
    -
    - { backButton } - { e2eIconElement } -

    { memberName }

    -
    - { avatarElement } -
    - -
    -
    - { this.props.member.userId } -
    - { roomMemberDetails } -
    -
    - -
    - { this._renderUserOptions() } - - { adminTools } - - { startChat } - - { this._renderDevices() } - - { spinner } -
    -
    -
    - ); - }, -}); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 2d290564c3..3be378b341 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -57,21 +57,19 @@ export default createReactClass({ } } - if (SettingsStore.getValue("feature_cross_signing")) { - const { roomId } = this.props.member; - if (roomId) { - const isRoomEncrypted = cli.isRoomEncrypted(roomId); - this.setState({ - isRoomEncrypted, - }); - if (isRoomEncrypted) { - cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged); - cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); - this.updateE2EStatus(); - } else { - // Listen for room to become encrypted - cli.on("RoomState.events", this.onRoomStateEvents); - } + const { roomId } = this.props.member; + if (roomId) { + const isRoomEncrypted = cli.isRoomEncrypted(roomId); + this.setState({ + isRoomEncrypted, + }); + if (isRoomEncrypted) { + cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged); + cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); + this.updateE2EStatus(); + } else { + // Listen for room to become encrypted + cli.on("RoomState.events", this.onRoomStateEvents); } } }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 732df3dbbf..84a5a3a9a0 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -27,6 +27,7 @@ import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; +import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -103,6 +104,32 @@ HangupButton.propTypes = { roomId: PropTypes.string.isRequired, }; +const EmojiButton = ({addEmoji}) => { + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const buttonRect = button.current.getBoundingClientRect(); + const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); + contextMenu = + + ; + } + + return + + + + + { contextMenu } + ; +}; + class UploadButton extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -281,37 +308,28 @@ export default class MessageComposer extends React.Component { } renderPlaceholderText() { - if (SettingsStore.getValue("feature_cross_signing")) { - if (this.state.isQuoting) { - if (this.props.e2eStatus) { - return _t('Send an encrypted reply…'); - } else { - return _t('Send a reply…'); - } + if (this.state.isQuoting) { + if (this.props.e2eStatus) { + return _t('Send an encrypted reply…'); } else { - if (this.props.e2eStatus) { - return _t('Send an encrypted message…'); - } else { - return _t('Send a message…'); - } + return _t('Send a reply…'); } } else { - if (this.state.isQuoting) { - if (this.props.e2eStatus) { - return _t('Send an encrypted reply…'); - } else { - return _t('Send a reply (unencrypted)…'); - } + if (this.props.e2eStatus) { + return _t('Send an encrypted message…'); } else { - if (this.props.e2eStatus) { - return _t('Send an encrypted message…'); - } else { - return _t('Send a message (unencrypted)…'); - } + return _t('Send a message…'); } } } + addEmoji(emoji) { + dis.dispatch({ + action: "insert_emoji", + emoji, + }); + } + render() { const controls = [ this.state.me ? : null, @@ -335,8 +353,9 @@ export default class MessageComposer extends React.Component { room={this.props.room} placeholder={this.renderPlaceholderText()} permalinkCreator={this.props.permalinkCreator} />, - , , + , + , ); if (this.state.showCallButtons) { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 17495e6299..4820d0c8ff 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -168,10 +168,8 @@ export default createReactClass({ const joinRule = joinRules && joinRules.getContent().join_rule; let privateIcon; // Don't show an invite-only icon for DMs. Users know they're invite-only. - if (!dmUserId && SettingsStore.getValue("feature_cross_signing")) { - if (joinRule == "invite") { - privateIcon = ; - } + if (!dmUserId && joinRule === "invite") { + privateIcon = ; } if (this.props.onCancelClick) { diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index d0c147c953..15aa880109 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -18,16 +18,17 @@ limitations under the License. import * as React from "react"; import { _t, _td } from "../../../languageHandler"; -import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2"; import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import dis from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; import { ActionPayload } from "../../../dispatcher/payloads"; +import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; +import { ListLayout } from "../../../stores/room-list/ListLayout"; /******************************************************************* * CAUTION * @@ -48,6 +49,7 @@ interface IProps { interface IState { sublists: ITagMap; + layouts: Map; } const TAG_ORDER: TagID[] = [ @@ -125,64 +127,43 @@ const TAG_AESTHETICS: { }; export default class RoomList2 extends React.Component { - private sublistRefs: { [tagId: string]: React.RefObject } = {}; - private sublistSizes: { [tagId: string]: number } = {}; - private sublistCollapseStates: { [tagId: string]: boolean } = {}; - private unfilteredLayout: Layout; - private filteredLayout: Layout; + private searchFilter: NameFilterCondition = new NameFilterCondition(); constructor(props: IProps) { super(props); - this.state = {sublists: {}}; - this.loadSublistSizes(); - this.prepareLayouts(); + this.state = { + sublists: {}, + layouts: new Map(), + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + if (prevProps.searchFilter !== this.props.searchFilter) { + const hadSearch = !!this.searchFilter.search.trim(); + const haveSearch = !!this.props.searchFilter.trim(); + this.searchFilter.search = this.props.searchFilter; + if (!hadSearch && haveSearch) { + // started a new filter - add the condition + RoomListStore.instance.addFilter(this.searchFilter); + } else if (hadSearch && !haveSearch) { + // cleared a filter - remove the condition + RoomListStore.instance.removeFilter(this.searchFilter); + } // else the filter hasn't changed enough for us to care here + } } public componentDidMount(): void { - RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { - console.log("new lists", store.orderedLists); - this.setState({sublists: store.orderedLists}); - }); - } + RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => { + const newLists = store.orderedLists; + console.log("new lists", newLists); - private loadSublistSizes() { - const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); - if (sizesJson) this.sublistSizes = JSON.parse(sizesJson); - - const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); - if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson); - } - - private saveSublistSizes() { - window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes)); - window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates)); - } - - private prepareLayouts() { - // TODO: Change layout engine for FTUE support - this.unfilteredLayout = new Layout((tagId: string, height: number) => { - const sublist = this.sublistRefs[tagId]; - if (sublist) sublist.current.setHeight(height); - - // TODO: Check overflow (see old impl) - - // Don't store a height for collapsed sublists - if (!this.sublistCollapseStates[tagId]) { - this.sublistSizes[tagId] = height; - this.saveSublistSizes(); + const layoutMap = new Map(); + for (const tagId of Object.keys(newLists)) { + layoutMap.set(tagId, new ListLayout(tagId)); } - }, this.sublistSizes, this.sublistCollapseStates, { - allowWhitespace: false, - handleHeight: 1, - }); - this.filteredLayout = new Layout((tagId: string, height: number) => { - const sublist = this.sublistRefs[tagId]; - if (sublist) sublist.current.setHeight(height); - }, null, null, { - allowWhitespace: false, - handleHeight: 0, + this.setState({sublists: newLists, layouts: layoutMap}); }); } @@ -208,16 +189,19 @@ export default class RoomList2 extends React.Component { if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; - components.push(); + components.push( + + ); } return components; @@ -232,7 +216,7 @@ export default class RoomList2 extends React.Component { onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyDown={onKeyDownHandler} - className="mx_RoomList" + className="mx_RoomList mx_RoomList2" role="tree" aria-label={_t("Rooms")} // Firefox sometimes makes this element focusable due to diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index e2f489b959..d3bb19729d 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -20,7 +20,6 @@ import * as React from "react"; import { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; -import IndicatorScrollbar from "../../structures/IndicatorScrollbar"; import * as RoomNotifs from '../../../RoomNotifs'; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { _t } from "../../../languageHandler"; @@ -28,6 +27,8 @@ import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton"; import * as FormattingUtils from '../../../utils/FormattingUtils'; import RoomTile2 from "./RoomTile2"; +import { ResizableBox, ResizeCallbackData } from "react-resizable"; +import { ListLayout } from "../../../stores/room-list/ListLayout"; /******************************************************************* * CAUTION * @@ -45,9 +46,9 @@ interface IProps { onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; + layout: ListLayout; // TODO: Collapsed state - // TODO: Height // TODO: Group invites // TODO: Calls // TODO: forceExpand? @@ -61,10 +62,6 @@ interface IState { export default class RoomSublist2 extends React.Component { private headerButton = createRef(); - public setHeight(size: number) { - // TODO: Do a thing (maybe - height changes are different in FTUE) - } - private hasTiles(): boolean { return this.numTiles > 0; } @@ -79,6 +76,18 @@ export default class RoomSublist2 extends React.Component { if (this.props.onAddRoom) this.props.onAddRoom(); }; + private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => { + const direction = e.movementY < 0 ? -1 : +1; + const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction; + this.props.layout.visibleTiles += tileDiff; + this.forceUpdate(); // because the layout doesn't trigger a re-render + }; + + private onShowAllClick = () => { + this.props.layout.visibleTiles = this.numTiles; + this.forceUpdate(); // because the layout doesn't trigger a re-render + }; + private renderTiles(): React.ReactElement[] { const tiles: React.ReactElement[] = []; @@ -204,10 +213,57 @@ export default class RoomSublist2 extends React.Component { if (tiles.length > 0) { // TODO: Lazy list rendering // TODO: Whatever scrolling magic needs to happen here + const layout = this.props.layout; // to shorten calls + const minTilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.minVisibleTiles)); + const maxTilesPx = layout.tilesToPixels(tiles.length); + const tilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.visibleTiles)); + let handles = ['s']; + if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) { + handles = []; // no handles, we're at a minimum + } + + // TODO: This might need adjustment, however for now it is fine as a round. + const nVisible = Math.round(layout.visibleTiles); + const visibleTiles = tiles.slice(0, nVisible); + + // If we're hiding rooms, show a 'show more' button to the user. This button + // replaces the last visible tile, so will always show 2+ rooms. We do this + // because if it said "show 1 more room" we had might as well show that room + // instead. We also replace the last item so we don't have to adjust our math + // on pixel heights, etc. It's much easier to pretend the button is a tile. + if (tiles.length > nVisible) { + // we have a cutoff condition - add the button to show all + + // we +1 to account for the room we're about to hide with our 'show more' button + // this results in the button always being 1+, and not needing an i18n `count`. + const numMissing = (tiles.length - visibleTiles.length) + 1; + + // TODO: CSS TBD + // TODO: Make this an actual tile + // TODO: This is likely to pop out of the list, consider that. + visibleTiles.splice(visibleTiles.length - 1, 1, ( +
    + {_t("Show %(n)s more", {n: numMissing})} +
    + )); + } content = ( - - {tiles} - + + {visibleTiles} + ) } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 44e5ae7643..5917f2ae77 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -155,9 +155,6 @@ export default createReactClass({ if (!cli.isRoomEncrypted(this.props.room.roomId)) { return; } - if (!SettingsStore.getValue("feature_cross_signing")) { - return; - } /* At this point, the user has encryption on and cross-signing on */ this.setState({ @@ -515,10 +512,8 @@ export default createReactClass({ } let privateIcon = null; - if (SettingsStore.getValue("feature_cross_signing")) { - if (this.state.joinRule == "invite" && !dmUserId) { - privateIcon = ; - } + if (this.state.joinRule === "invite" && !dmUserId) { + privateIcon = ; } let e2eIcon = null; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8b1beee713..c95cd108dc 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -30,6 +30,8 @@ import * as RoomNotifs from '../../../RoomNotifs'; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; import * as Unread from '../../../Unread'; import * as FormattingUtils from "../../../utils/FormattingUtils"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; /******************************************************************* * CAUTION * @@ -86,10 +88,22 @@ export default class RoomTile2 extends React.Component { hover: false, notificationState: this.getNotificationState(), }; + + this.props.room.on("Room.receipt", this.handleRoomEventUpdate); + this.props.room.on("Room.timeline", this.handleRoomEventUpdate); + this.props.room.on("Room.redaction", this.handleRoomEventUpdate); + MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); } public componentWillUnmount() { - // TODO: Listen for changes to the badge count and update as needed + if (this.props.room) { + this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate); + this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate); + this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate); + } + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); + } } // XXX: This is a bit of an awful-looking hack. We should probably be using state for @@ -99,7 +113,15 @@ export default class RoomTile2 extends React.Component { return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite; } - // TODO: Make use of this function when the notification state needs updating. + private handleRoomEventUpdate = (event: MatrixEvent) => { + const roomId = event.getRoomId(); + + // Sanity check: should never happen + if (roomId !== this.props.room.roomId) return; + + this.updateNotificationState(); + }; + private updateNotificationState() { this.setState({notificationState: this.getNotificationState()}); } @@ -214,7 +236,7 @@ export default class RoomTile2 extends React.Component { let tooltip = null; if (false) { // isCollapsed if (this.state.hover) { - tooltip = + tooltip = } } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 5ea979a8ef..25ad192ea4 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -44,6 +44,7 @@ import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RateLimitedFunc from '../../../ratelimitedfunc'; +import {Action} from "../../../dispatcher/actions"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -312,6 +313,7 @@ export default class SendMessageComposer extends React.Component { event: null, }); } + dis.dispatch({action: "message_sent"}); } this.sendHistoryManager.save(this.model); @@ -322,13 +324,8 @@ export default class SendMessageComposer extends React.Component { this._clearStoredEditorState(); } - componentDidMount() { - this._editorRef.getEditableRootNode().addEventListener("paste", this._onPaste, true); - } - componentWillUnmount() { dis.unregister(this.dispatcherRef); - this._editorRef.getEditableRootNode().removeEventListener("paste", this._onPaste, true); } // TODO: [REACT-WARNING] Move this to constructor @@ -368,7 +365,7 @@ export default class SendMessageComposer extends React.Component { onAction = (payload) => { switch (payload.action) { case 'reply_to_event': - case 'focus_composer': + case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; case 'insert_mention': @@ -377,6 +374,9 @@ export default class SendMessageComposer extends React.Component { case 'quote': this._insertQuotedMessage(payload.event); break; + case 'insert_emoji': + this._insertEmoji(payload.emoji); + break; } }; @@ -414,6 +414,17 @@ export default class SendMessageComposer extends React.Component { this._editorRef && this._editorRef.focus(); } + _insertEmoji = (emoji) => { + const {model} = this; + const {partCreator} = model; + const caret = this._editorRef.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([partCreator.plain(emoji)], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + }; + _onPaste = (event) => { const {clipboardData} = event; if (clipboardData.files.length) { @@ -424,6 +435,7 @@ export default class SendMessageComposer extends React.Component { ContentMessages.sharedInstance().sendContentListToRoom( Array.from(clipboardData.files), this.props.room.roomId, this.context, ); + return true; // to skip internal onPaste handler } } @@ -440,6 +452,7 @@ export default class SendMessageComposer extends React.Component { label={this.props.placeholder} placeholder={this.props.placeholder} onChange={this._saveStoredEditorState} + onPaste={this._onPaste} />
    ); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index c7eccf2145..a4ff65d8f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -141,6 +141,12 @@ export default createReactClass({ _changePassword: function(cli, oldPassword, newPassword) { const authDict = { type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: cli.credentials.userId, + }, + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 user: cli.credentials.userId, password: oldPassword, }; diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index b1642e260d..7eb239cbca 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -194,6 +194,8 @@ export default class CrossSigningPanel extends React.PureComponent {
    ); } + + // TODO: determine how better to expose this to users in addition to prompts at login/toast let bootstrapButton; if ( (!enabledForAccount || !crossSigningPublicKeysOnDevice) && diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index ff3c318a92..567b144a92 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {formatDate} from '../../../DateUtils'; +import StyledCheckbox from '../elements/StyledCheckbox'; export default class DevicesPanelEntry extends React.Component { constructor(props) { @@ -81,7 +82,7 @@ export default class DevicesPanelEntry extends React.Component { { lastSeen }
    - +
    ); diff --git a/src/components/views/settings/EnableNotificationsButton.js b/src/components/views/settings/EnableNotificationsButton.js deleted file mode 100644 index e4b348dfbd..0000000000 --- a/src/components/views/settings/EnableNotificationsButton.js +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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 createReactClass from 'create-react-class'; -import Notifier from "../../../Notifier"; -import dis from "../../../dispatcher/dispatcher"; -import { _t } from '../../../languageHandler'; - -export default createReactClass({ - displayName: 'EnableNotificationsButton', - - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - }, - - componentWillUnmount: function() { - dis.unregister(this.dispatcherRef); - }, - - onAction: function(payload) { - if (payload.action !== "notifier_enabled") { - return; - } - this.forceUpdate(); - }, - - enabled: function() { - return Notifier.isEnabled(); - }, - - onClick: function() { - const self = this; - if (!Notifier.supportsDesktopNotifications()) { - return; - } - if (!Notifier.isEnabled()) { - Notifier.setEnabled(true, function() { - self.forceUpdate(); - }); - } else { - Notifier.setEnabled(false); - } - this.forceUpdate(); - }, - - render: function() { - if (this.enabled()) { - return ( - - ); - } else { - return ( - - ); - } - }, -}); diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index fa3fa03c74..a7a2c768db 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,7 +21,6 @@ import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; -import SettingsStore from '../../../settings/SettingsStore'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -316,7 +315,7 @@ export default class KeyBackupPanel extends React.PureComponent { trustedLocally = _t("This backup is trusted because it has been restored on this session"); } - let buttonRow = ( + const buttonRow = (
    {restoreButtonCaption} @@ -326,13 +325,6 @@ export default class KeyBackupPanel extends React.PureComponent {
    ); - if (this.state.backupKeyStored && !SettingsStore.getValue("feature_cross_signing")) { - buttonRow =

    ⚠️ {_t( - "Backup key stored in secret storage, but this feature is not " + - "enabled on this session. Please enable cross-signing in Labs to " + - "modify key backup state.", - )}

    ; - } return
    {clientBackupStatus}
    diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 23ab60423a..5dbdcd4901 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -40,8 +40,8 @@ export default class ProfileSettings extends React.Component { if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); this.state = { userId: user.userId, - originalDisplayName: user.displayName, - displayName: user.displayName, + originalDisplayName: user.rawDisplayName, + displayName: user.rawDisplayName, originalAvatarUrl: avatarUrl, avatarUrl: avatarUrl, avatarFile: null, diff --git a/src/components/views/settings/UpdateCheckButton.tsx b/src/components/views/settings/UpdateCheckButton.tsx new file mode 100644 index 0000000000..10e0e29f31 --- /dev/null +++ b/src/components/views/settings/UpdateCheckButton.tsx @@ -0,0 +1,88 @@ +/* +Copyright 2020 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 {UpdateCheckStatus} from "../../../BasePlatform"; +import PlatformPeg from "../../../PlatformPeg"; +import {useDispatcher} from "../../../hooks/useDispatcher"; +import dis from "../../../dispatcher/dispatcher"; +import {Action} from "../../../dispatcher/actions"; +import {_t} from "../../../languageHandler"; +import InlineSpinner from "../../../components/views/elements/InlineSpinner"; +import AccessibleButton from "../../../components/views/elements/AccessibleButton"; +import {CheckUpdatesPayload} from "../../../dispatcher/payloads/CheckUpdatesPayload"; + +function installUpdate() { + PlatformPeg.get().installUpdate(); +} + +function getStatusText(status: UpdateCheckStatus, errorDetail?: string) { + switch (status) { + case UpdateCheckStatus.Error: + return _t('Error encountered (%(errorDetail)s).', { errorDetail }); + case UpdateCheckStatus.Checking: + return _t('Checking for an update...'); + case UpdateCheckStatus.NotAvailable: + return _t('No update available.'); + case UpdateCheckStatus.Downloading: + return _t('Downloading update...'); + case UpdateCheckStatus.Ready: + return _t("New version available. Update now.", {}, { + a: sub => {sub} + }); + } +} + +const doneStatuses = [ + UpdateCheckStatus.Ready, + UpdateCheckStatus.Error, + UpdateCheckStatus.NotAvailable, +]; + +const UpdateCheckButton = () => { + const [state, setState] = useState(null); + + const onCheckForUpdateClick = () => { + setState(null); + PlatformPeg.get().startUpdateCheck(); + }; + + useDispatcher(dis, ({action, ...params}) => { + if (action === Action.CheckUpdates) { + setState(params as CheckUpdatesPayload); + } + }); + + const busy = state && !doneStatuses.includes(state.status); + + let suffix; + if (state) { + suffix = + {getStatusText(state.status, state.detail)} + {busy && } + ; + } + + return + + {_t("Check for update")} + + { suffix } + ; +}; + +export default UpdateCheckButton; diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index ad2dabd8ae..02e995ac45 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -267,7 +267,7 @@ export default class PhoneNumbers extends React.Component { label={_t("Phone Number")} autoComplete="off" disabled={this.state.verifying} - prefix={phoneCountry} + prefixComponent={phoneCountry} value={this.state.newPhoneNumber} onChange={this._onChangeNewPhoneNumber} /> diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index eb2b885a22..c67596a3a5 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -247,7 +247,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
    - {_t("To link to this room, please add an alias.")} + {_t("To link to this room, please add an address.")}
    ); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx similarity index 73% rename from src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js rename to src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 5b49dd0abd..bcd87b290a 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -20,34 +20,64 @@ import React from 'react'; import {_t} from "../../../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; import * as sdk from "../../../../../index"; -import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; +import { enumerateThemes } from "../../../../../theme"; +import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher"; import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher/dispatcher"; -import { FontWatcher } from "../../../../../FontWatcher"; +import { FontWatcher } from "../../../../../settings/watchers/FontWatcher"; +import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload'; +import { Action } from '../../../../../dispatcher/actions'; +import { IValidationResult, IFieldState } from '../../../elements/Validation'; -export default class AppearanceUserSettingsTab extends React.Component { - constructor() { - super(); +interface IProps { +} + +interface IThemeState { + theme: string, + useSystemTheme: boolean, +} + +export interface CustomThemeMessage { + isError: boolean, + text: string +}; + +interface IState extends IThemeState { + // String displaying the current selected fontSize. + // Needs to be string for things like '17.' without + // trailing 0s. + fontSize: string, + customThemeUrl: string, + customThemeMessage: CustomThemeMessage, + useCustomFontSize: boolean, +} + +export default class AppearanceUserSettingsTab extends React.Component { + + private themeTimer: NodeJS.Timeout; + + constructor(props: IProps) { + super(props); this.state = { - fontSize: SettingsStore.getValue("fontSize", null), - ...this._calculateThemeState(), + fontSize: SettingsStore.getValue("fontSize", null).toString(), + ...this.calculateThemeState(), customThemeUrl: "", customThemeMessage: {isError: false, text: ""}, useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), }; } - _calculateThemeState() { + private calculateThemeState(): IThemeState { // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we // show the right values for things. - const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); - const systemThemeExplicit = SettingsStore.getValueAt( + const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); + const systemThemeExplicit: boolean = SettingsStore.getValueAt( SettingLevel.DEVICE, "use_system_theme", null, false, true); - const themeExplicit = SettingsStore.getValueAt( + const themeExplicit: string = SettingsStore.getValueAt( SettingLevel.DEVICE, "theme", null, false, true); // If the user has enabled system theme matching, use that. @@ -73,15 +103,15 @@ export default class AppearanceUserSettingsTab extends React.Component { }; } - _onThemeChange = (e) => { + private onThemeChange = (e: React.ChangeEvent): void => { const newTheme = e.target.value; if (this.state.theme === newTheme) return; // doing getValue in the .catch will still return the value we failed to set, // so remember what the value was before we tried to set it so we can revert - const oldTheme = SettingsStore.getValue('theme'); + const oldTheme: string = SettingsStore.getValue('theme'); SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { - dis.dispatch({action: 'recheck_theme'}); + dis.dispatch({action: Action.RecheckTheme}); this.setState({theme: oldTheme}); }); this.setState({theme: newTheme}); @@ -91,23 +121,21 @@ export default class AppearanceUserSettingsTab extends React.Component { // XXX: The local echoed value appears to be unreliable, in particular // when settings custom themes(!) so adding forceTheme to override // the value from settings. - dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); + dis.dispatch({action: Action.RecheckTheme, forceTheme: newTheme}); }; - _onUseSystemThemeChanged = (checked) => { + private onUseSystemThemeChanged = (checked: boolean): void => { this.setState({useSystemTheme: checked}); SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({action: 'recheck_theme'}); + dis.dispatch({action: Action.RecheckTheme}); }; - _onFontSizeChanged = (size) => { - this.setState({fontSize: size}); + private onFontSizeChanged = (size: number): void => { + this.setState({fontSize: size.toString()}); SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); }; - _onValidateFontSize = ({value}) => { - console.log({value}); - + private onValidateFontSize = async ({value}: Pick): Promise => { const parsedSize = parseFloat(value); const min = FontWatcher.MIN_SIZE; const max = FontWatcher.MAX_SIZE; @@ -127,17 +155,18 @@ export default class AppearanceUserSettingsTab extends React.Component { return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; } - _onAddCustomTheme = async () => { - let currentThemes = SettingsStore.getValue("custom_themes"); + private onAddCustomTheme = async (): Promise => { + let currentThemes: string[] = SettingsStore.getValue("custom_themes"); if (!currentThemes) currentThemes = []; currentThemes = currentThemes.map(c => c); // cheap clone - if (this._themeTimer) { - clearTimeout(this._themeTimer); + if (this.themeTimer) { + clearTimeout(this.themeTimer); } try { const r = await fetch(this.state.customThemeUrl); + // XXX: need some schema for this const themeInfo = await r.json(); if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') { this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}}); @@ -153,42 +182,32 @@ export default class AppearanceUserSettingsTab extends React.Component { await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); - this._themeTimer = setTimeout(() => { + this.themeTimer = setTimeout(() => { this.setState({customThemeMessage: {text: "", isError: false}}); }, 3000); }; - _onCustomThemeChange = (e) => { + private onCustomThemeChange = (e: React.ChangeEvent): void => { this.setState({customThemeUrl: e.target.value}); }; - render() { - return ( -
    -
    {_t("Appearance")}
    - {this._renderThemeSection()} - {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null} -
    - ); - } - - _renderThemeSection() { + private renderThemeSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); const themeWatcher = new ThemeWatcher(); - let systemThemeSection; + let systemThemeSection: JSX.Element; if (themeWatcher.isSystemThemeSupported()) { systemThemeSection =
    ; } - let customThemeForm; + let customThemeForm: JSX.Element; if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { let messageElement = null; if (this.state.customThemeMessage.text) { @@ -200,17 +219,17 @@ export default class AppearanceUserSettingsTab extends React.Component { } customThemeForm = (
    -
    + {_t("Add theme")} @@ -220,7 +239,8 @@ export default class AppearanceUserSettingsTab extends React.Component { ); } - const themes = Object.entries(enumerateThemes()) + // XXX: replace any type here + const themes = Object.entries(enumerateThemes()) .map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const customThemes = themes.filter(p => !builtInThemes.includes(p)) @@ -232,7 +252,7 @@ export default class AppearanceUserSettingsTab extends React.Component { {systemThemeSection} {orderedThemes.map(theme => { @@ -245,7 +265,7 @@ export default class AppearanceUserSettingsTab extends React.Component { ); } - _renderFontSection() { + private renderFontSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return
    {_t("Font size")} @@ -253,9 +273,9 @@ export default class AppearanceUserSettingsTab extends React.Component {
    Aa
    {}} + value={parseInt(this.state.fontSize, 10)} + onSelectionChange={this.onFontSizeChanged} + displayFunc={value => ""} disabled={this.state.useCustomFontSize} />
    Aa
    @@ -263,7 +283,7 @@ export default class AppearanceUserSettingsTab extends React.Component { this.setState({useCustomFontSize: checked})} + onChange={(checked) => this.setState({useCustomFontSize: checked})} /> this.setState({fontSize: value.target.value})} disabled={!this.state.useCustomFontSize} />
    ; } + + render() { + return ( +
    +
    {_t("Appearance")}
    + {this.renderThemeSection()} + {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null} +
    + ); + } } diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 146d841d58..bec79b97c4 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -25,6 +25,7 @@ import Modal from "../../../../../Modal"; import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; +import UpdateCheckButton from "../../UpdateCheckButton"; export default class HelpUserSettingsTab extends React.Component { static propTypes = { @@ -177,12 +178,7 @@ export default class HelpUserSettingsTab extends React.Component { let updateButton = null; if (this.state.canUpdate) { - const platform = PlatformPeg.get(); - updateButton = ( - - {_t('Check for update')} - - ); + updateButton = ; } return ( diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index d22b7ec183..f1fe5f2556 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -84,7 +84,7 @@ export default class MjolnirUserSettingsTab extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { title: _t('Error subscribing to list'), - description: _t('Please verify the room ID or alias and try again.'), + description: _t('Please verify the room ID or address and try again.'), }); } finally { this.setState({busy: false}); @@ -305,7 +305,7 @@ export default class MjolnirUserSettingsTab extends React.Component { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index bed057f03d..952d9f1e78 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../../../languageHandler"; -import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; +import {SettingLevel} from "../../../../../settings/SettingsStore"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as FormattingUtils from "../../../../../utils/FormattingUtils"; import AccessibleButton from "../../../elements/AccessibleButton"; @@ -26,6 +26,7 @@ import Modal from "../../../../../Modal"; import * as sdk from "../../../../.."; import {sleep} from "../../../../../utils/promise"; import dis from "../../../../../dispatcher/dispatcher"; +import {privateShouldBeEncrypted} from "../../../../../createRoom"; export class IgnoredUser extends React.Component { static propTypes = { @@ -306,9 +307,7 @@ export default class SecurityUserSettingsTab extends React.Component { // in having advanced details here once all flows are implemented, we // can remove this. const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel'); - let crossSigning; - if (SettingsStore.getValue("feature_cross_signing")) { - crossSigning = ( + const crossSigning = (
    {_t("Cross-signing")}
    @@ -316,12 +315,20 @@ export default class SecurityUserSettingsTab extends React.Component {
    ); - } const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel'); + let warning; + if (!privateShouldBeEncrypted()) { + warning =
    + { _t("Your server admin has disabled end-to-end encryption by default " + + "in private rooms & Direct Messages.") } +
    ; + } + return (
    + {warning}
    {_t("Security & Privacy")}
    {_t("Where you’re logged in")} diff --git a/src/components/views/toasts/BulkUnverifiedSessionsToast.js b/src/components/views/toasts/BulkUnverifiedSessionsToast.js deleted file mode 100644 index 99ff529c35..0000000000 --- a/src/components/views/toasts/BulkUnverifiedSessionsToast.js +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2020 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 PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import dis from "../../../dispatcher/dispatcher"; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import DeviceListener from '../../../DeviceListener'; -import FormButton from '../elements/FormButton'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; - -@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast") -export default class BulkUnverifiedSessionsToast extends React.PureComponent { - static propTypes = { - deviceIds: PropTypes.array, - } - - _onLaterClick = () => { - DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds); - }; - - _onReviewClick = async () => { - DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds); - - dis.dispatch({ - action: 'view_user_info', - userId: MatrixClientPeg.get().getUserId(), - }); - }; - - render() { - return (
    -
    - {_t("Verify all your sessions to ensure your account & messages are safe")} -
    -
    - - -
    -
    ); - } -} diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx new file mode 100644 index 0000000000..ea12641948 --- /dev/null +++ b/src/components/views/toasts/GenericToast.tsx @@ -0,0 +1,46 @@ +/* +Copyright 2020 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, {ReactChild} from "react"; + +import FormButton from "../elements/FormButton"; +import {XOR} from "../../../@types/common"; + +interface IProps { + description: ReactChild; + acceptLabel: string; + + onAccept(); +} + +interface IPropsExtended extends IProps { + rejectLabel: string; + onReject(); +} + +const GenericToast: React.FC> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => { + return
    +
    + { description } +
    +
    + {onReject && rejectLabel && } + +
    +
    ; +}; + +export default GenericToast; diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js deleted file mode 100644 index b5510e85b6..0000000000 --- a/src/components/views/toasts/SetupEncryptionToast.js +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2020 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 PropTypes from 'prop-types'; -import Modal from '../../../Modal'; -import * as sdk from "../../../index"; -import { _t } from '../../../languageHandler'; -import DeviceListener from '../../../DeviceListener'; -import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog"; -import { accessSecretStorage } from '../../../CrossSigningManager'; - -export default class SetupEncryptionToast extends React.PureComponent { - static propTypes = { - toastKey: PropTypes.string.isRequired, - kind: PropTypes.oneOf([ - 'set_up_encryption', - 'verify_this_session', - 'upgrade_encryption', - ]).isRequired, - }; - - _onLaterClick = () => { - DeviceListener.sharedInstance().dismissEncryptionSetup(); - }; - - _onSetupClick = async () => { - if (this.props.kind === "verify_this_session") { - Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog, - {}, null, /* priority = */ false, /* static = */ true); - } else { - const Spinner = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog( - Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true, - ); - try { - await accessSecretStorage(); - } finally { - modal.close(); - } - } - }; - - getDescription() { - switch (this.props.kind) { - case 'set_up_encryption': - case 'upgrade_encryption': - return _t('Verify yourself & others to keep your chats safe'); - case 'verify_this_session': - return _t('Other users may not trust it'); - } - } - - getSetupCaption() { - switch (this.props.kind) { - case 'set_up_encryption': - return _t('Set up'); - case 'upgrade_encryption': - return _t('Upgrade'); - case 'verify_this_session': - return _t('Verify'); - } - } - - render() { - const FormButton = sdk.getComponent("elements.FormButton"); - return (
    -
    {this.getDescription()}
    -
    - - -
    -
    ); - } -} diff --git a/src/components/views/toasts/UnverifiedSessionToast.js b/src/components/views/toasts/UnverifiedSessionToast.js deleted file mode 100644 index 38cd9f20df..0000000000 --- a/src/components/views/toasts/UnverifiedSessionToast.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright 2020 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 PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import Modal from '../../../Modal'; -import DeviceListener from '../../../DeviceListener'; -import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog'; -import FormButton from '../elements/FormButton'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; - -@replaceableComponent("views.toasts.UnverifiedSessionToast") -export default class UnverifiedSessionToast extends React.PureComponent { - static propTypes = { - deviceId: PropTypes.string, - } - - _onLaterClick = () => { - DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]); - }; - - _onReviewClick = async () => { - const cli = MatrixClientPeg.get(); - Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, { - userId: cli.getUserId(), - device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId), - onFinished: (r) => { - if (!r) { - /* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */ - DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]); - } - }, - }, null, /* priority = */ false, /* static = */ true); - }; - - render() { - const cli = MatrixClientPeg.get(); - const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId); - - return (
    -
    - {_t( - "Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})} -
    -
    - - -
    -
    ); - } -} diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.tsx similarity index 86% rename from src/components/views/toasts/VerificationRequestToast.js rename to src/components/views/toasts/VerificationRequestToast.tsx index 421dd7bea1..38e7e31989 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from "react"; + import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; @@ -24,8 +24,23 @@ import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver import dis from "../../../dispatcher/dispatcher"; import ToastStore from "../../../stores/ToastStore"; import Modal from "../../../Modal"; +import GenericToast from "./GenericToast"; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo"; + +interface IProps { + toastKey: string; + request: VerificationRequest; +} + +interface IState { + counter: number; + device?: DeviceInfo; +} + +export default class VerificationRequestToast extends React.PureComponent { + private intervalHandle: NodeJS.Timeout; -export default class VerificationRequestToast extends React.PureComponent { constructor(props) { super(props); this.state = {counter: Math.ceil(props.request.timeout / 1000)}; @@ -34,7 +49,7 @@ export default class VerificationRequestToast extends React.PureComponent { async componentDidMount() { const {request} = this.props; if (request.timeout && request.timeout > 0) { - this._intervalHandle = setInterval(() => { + this.intervalHandle = setInterval(() => { let {counter} = this.state; counter = Math.max(0, counter - 1); this.setState({counter}); @@ -56,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent { } componentWillUnmount() { - clearInterval(this._intervalHandle); + clearInterval(this.intervalHandle); const {request} = this.props; request.off("change", this._checkRequestIsPending); } @@ -110,7 +125,6 @@ export default class VerificationRequestToast extends React.PureComponent { }; render() { - const FormButton = sdk.getComponent("elements.FormButton"); const {request} = this.props; let nameLabel; if (request.isSelfVerification) { @@ -133,20 +147,16 @@ export default class VerificationRequestToast extends React.PureComponent { } } } - const declineLabel = this.state.counter == 0 ? + const declineLabel = this.state.counter === 0 ? _t("Decline") : _t("Decline (%(counter)s)", {counter: this.state.counter}); - return (
    -
    {nameLabel}
    -
    - - -
    -
    ); + + return ; } } - -VerificationRequestToast.propTypes = { - request: PropTypes.object.isRequired, - toastKey: PropTypes.string.isRequired, -}; diff --git a/src/createRoom.js b/src/createRoom.js index 18fc787e1c..affdf196a7 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -23,7 +23,8 @@ import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; -import SettingsStore from "./settings/SettingsStore"; + +const E2EE_WK_KEY = "im.vector.riot.e2ee"; /** * Create a new room, and switch to it. @@ -227,7 +228,7 @@ export async function ensureDMExists(client, userId) { roomId = existingDMRoom.roomId; } else { let encryption; - if (SettingsStore.getValue("feature_cross_signing")) { + if (privateShouldBeEncrypted()) { encryption = canEncryptToAllUsers(client, [userId]); } roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false}); @@ -235,3 +236,13 @@ export async function ensureDMExists(client, userId) { } return roomId; } + +export function privateShouldBeEncrypted() { + const clientWellKnown = MatrixClientPeg.get().getClientWellKnown(); + if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) { + const defaultDisabled = clientWellKnown[E2EE_WK_KEY]["default"] === false; + return !defaultDisabled; + } + + return true; +} diff --git a/src/cryptodevices.js b/src/cryptodevices.js deleted file mode 100644 index 86b97364f9..0000000000 --- a/src/cryptodevices.js +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -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 Resend from './Resend'; -import * as sdk from './index'; -import dis from './dispatcher/dispatcher'; -import Modal from './Modal'; -import { _t } from './languageHandler'; - -/** - * Mark all given devices as 'known' - * - * @param {MatrixClient} matrixClient A MatrixClient - * @param {Object} devices Map from userid -> deviceid -> deviceinfo - */ -export function markAllDevicesKnown(matrixClient, devices) { - Object.keys(devices).forEach((userId) => { - Object.keys(devices[userId]).map((deviceId) => { - matrixClient.setDeviceKnown(userId, deviceId, true); - }); - }); -} - -/** - * Gets all crypto devices in a room that are marked neither known - * nor verified. - * - * @param {MatrixClient} matrixClient A MatrixClient - * @param {Room} room js-sdk room object representing the room - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto~DeviceInfo|DeviceInfo}. - */ -export async function getUnknownDevicesForRoom(matrixClient, room) { - const roomMembers = (await room.getEncryptionTargetMembers()).map((m) => { - return m.userId; - }); - const devices = await matrixClient.downloadKeys(roomMembers, false); - const unknownDevices = {}; - // This is all devices in this room, so find the unknown ones. - Object.keys(devices).forEach((userId) => { - Object.keys(devices[userId]).map((deviceId) => { - const device = devices[userId][deviceId]; - - if (device.isUnverified() && !device.isKnown()) { - if (unknownDevices[userId] === undefined) { - unknownDevices[userId] = {}; - } - unknownDevices[userId][deviceId] = device; - } - }); - }); - return unknownDevices; -} - -function focusComposer() { - dis.dispatch({action: 'focus_composer'}); -} - -/** - * Show the UnknownDeviceDialog for a given room. The dialog will inform the user - * that messages they sent to this room have not been sent due to unknown devices - * being present. - * - * @param {MatrixClient} matrixClient A MatrixClient - * @param {Room} room js-sdk room object representing the room - */ -export function showUnknownDeviceDialogForMessages(matrixClient, room) { - getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => { - const onSendClicked = () => { - Resend.resendUnsentEvents(room); - }; - - const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); - Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, { - room: room, - devices: unknownDevices, - sendAnywayLabel: _t("Send anyway"), - sendLabel: _t("Send"), - onSend: onSendClicked, - onFinished: focusComposer, - }, 'mx_Dialog_unknownDevice'); - }); -} - -/** - * Show the UnknownDeviceDialog for a given room. The dialog will inform the user - * that a call they tried to place or answer in the room couldn't be placed or - * answered due to unknown devices being present. - * - * @param {MatrixClient} matrixClient A MatrixClient - * @param {Room} room js-sdk room object representing the room - * @param {func} sendAnyway Function called when the 'call anyway' or 'call' - * button is pressed. This should attempt to place or answer the call again. - * @param {string} sendAnywayLabel Label for the button displayed to retry the call - * when unknown devices are still present (eg. "Call Anyway") - * @param {string} sendLabel Label for the button displayed to retry the call - * after all devices have been verified (eg. "Call") - */ -export function showUnknownDeviceDialogForCalls(matrixClient, room, sendAnyway, sendAnywayLabel, sendLabel) { - getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => { - const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); - Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, { - room: room, - devices: unknownDevices, - sendAnywayLabel: sendAnywayLabel, - sendLabel: sendLabel, - onSend: sendAnyway, - }, 'mx_Dialog_unknownDevice'); - }); -} diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index a2f9c3efe3..71493d6e44 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -38,5 +38,24 @@ export enum Action { * Open the user settings. No additional payload information required. */ ViewUserSettings = "view_user_settings", -} + /** + * Sets the current tooltip. Should be use with ViewTooltipPayload. + */ + ViewTooltip = "view_tooltip", + + /** + * Forces the theme to reload. No additional payload information required. + */ + RecheckTheme = "recheck_theme", + + /** + * Provide status information for an ongoing update check. Should be used with a CheckUpdatesPayload. + */ + CheckUpdates = "check_updates", + + /** + * Focuses the user's cursor to the composer. No additional payload information required. + */ + FocusComposer = "focus_composer", +} diff --git a/src/dispatcher/payloads/CheckUpdatesPayload.ts b/src/dispatcher/payloads/CheckUpdatesPayload.ts new file mode 100644 index 0000000000..0f0f9a01e5 --- /dev/null +++ b/src/dispatcher/payloads/CheckUpdatesPayload.ts @@ -0,0 +1,33 @@ +/* +Copyright 2020 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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; +import {UpdateCheckStatus} from "../../BasePlatform"; + +export interface CheckUpdatesPayload extends ActionPayload { + action: Action.CheckUpdates, + + /** + * The current phase of the manual update check. + */ + status: UpdateCheckStatus; + + /** + * Detail string relating to the current status, typically for error details. + */ + detail?: string; +} diff --git a/res/css/views/dialogs/_EncryptedEventDialog.scss b/src/dispatcher/payloads/RecheckThemePayload.ts similarity index 55% rename from res/css/views/dialogs/_EncryptedEventDialog.scss rename to src/dispatcher/payloads/RecheckThemePayload.ts index ff73df509d..06f7012049 100644 --- a/res/css/views/dialogs/_EncryptedEventDialog.scss +++ b/src/dispatcher/payloads/RecheckThemePayload.ts @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2020 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. @@ -14,21 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_EncryptedEventDialog .mx_DeviceVerifyButtons { - float: right; - padding: 0px; - margin-right: 42px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; -.mx_EncryptedEventDialog .mx_MemberDeviceInfo_textButton { - @mixin mx_DialogButton; - background-color: $primary-bg-color; - color: $accent-color; -} +export interface RecheckThemePayload extends ActionPayload { + action: Action.RecheckTheme, -.mx_EncryptedEventDialog button { - margin-top: 0px; + /** + * Optionally specify the exact theme which is to be loaded. + */ + forceTheme?: string; } diff --git a/src/dispatcher/payloads/ViewTooltipPayload.ts b/src/dispatcher/payloads/ViewTooltipPayload.ts new file mode 100644 index 0000000000..8778287128 --- /dev/null +++ b/src/dispatcher/payloads/ViewTooltipPayload.ts @@ -0,0 +1,35 @@ +/* +Copyright 2020 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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; +import { Component } from "react"; + +export interface ViewTooltipPayload extends ActionPayload { + action: Action.ViewTooltip, + + /* + * The tooltip to render. If it's null the tooltip will not be rendered + * We need the void type because of typescript headaches. + */ + tooltip: null | void | Element | Component; + + /* + * The parent under which to render the tooltip. Can be null to remove + * the parent type. + */ + parent: null | Element +} \ No newline at end of file diff --git a/src/emoji.ts b/src/emoji.ts index c0a755145a..53625f3026 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// @ts-ignore - import * as EMOJIBASE actually breaks this import EMOJIBASE from 'emojibase-data/en/compact.json'; export interface IEmoji { @@ -63,6 +62,8 @@ export const DATA_BY_CATEGORY = { "flags": [], }; +const ZERO_WIDTH_JOINER = "\u200D"; + // Store various mappings from unicode/emoticon/shortcode to the Emoji objects EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; @@ -70,7 +71,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { DATA_BY_CATEGORY[categoryId].push(emoji); } // This is used as the string to match the query against when filtering emojis - emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase(); + emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + + `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`.toLowerCase(); // Add mapping from unicode to Emoji object // The 'unicode' field that we use in emojibase has either diff --git a/src/hooks/useDispatcher.ts b/src/hooks/useDispatcher.ts new file mode 100644 index 0000000000..004b15fcef --- /dev/null +++ b/src/hooks/useDispatcher.ts @@ -0,0 +1,40 @@ +/* +Copyright 2020 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 {useEffect, useRef} from "react"; + +import {ActionPayload} from "../dispatcher/payloads"; +import {Dispatcher} from "flux"; + +// Hook to simplify listening to flux dispatches +export const useDispatcher = (dispatcher: Dispatcher, handler: (payload: ActionPayload) => void) => { + // Create a ref that stores handler + const savedHandler = useRef((payload: ActionPayload) => {}); + + // Update ref.current value if handler changes. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Create event listener that calls handler function stored in ref + const ref = dispatcher.register((payload) => savedHandler.current(payload)); + // Remove event listener on cleanup + return () => { + dispatcher.unregister(ref); + }; + }, [dispatcher]); +}; diff --git a/src/hooks/useEventEmitter.js b/src/hooks/useEventEmitter.js index 7adc6ef2e3..6a758fb108 100644 --- a/src/hooks/useEventEmitter.js +++ b/src/hooks/useEventEmitter.js @@ -32,7 +32,7 @@ export const useEventEmitter = (emitter, eventName, handler) => { if (!emitter) return; // Create event listener that calls handler function stored in ref - const eventListener = event => savedHandler.current(event); + const eventListener = (...args) => savedHandler.current(...args); // Add event listener emitter.on(eventName, eventListener); diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 347823f95c..3d9bb8653c 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2211,5 +2211,118 @@ "Invite someone using their name, username (like ), email address or share this room.": "Поканете някой посредством име, потребителско име (като ), имейл адрес или като споделите тази стая.", "You added a new session '%(displayName)s', which is requesting encryption keys.": "Добавихте нова сесия '%(displayName)s', която изисква ключове за шифроване.", "Your unverified session '%(displayName)s' is requesting encryption keys.": "Непотвърдената ви сесия '%(displayName)s' изисква ключове за шифроване.", - "Loading session info...": "Зареждане на информация за сесията..." + "Loading session info...": "Зареждане на информация за сесията...", + "Opens chat with the given user": "Отваря чат с дадения потребител", + "Sends a message to the given user": "Изпраща съобщение до дадения потребител", + "Font scaling": "Мащабиране на шрифта", + "Use the improved room list (in development - refresh to apply changes)": "Използвай подобрения списък със стаи (в процес на разработка - презаредете за да приложите промените)", + "Use IRC layout": "Използвай IRC изглед", + "Font size": "Размер на шрифта", + "Custom font size": "Собствен размер на шрифта", + "IRC display name width": "Ширина на IRC името", + "Waiting for your other session to verify…": "Изчакване другата сесията да потвърди…", + "Size must be a number": "Размера трябва да е число", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Собствения размер на шрифта може да бъде единствено между %(min)s pt и %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Изберете между %(min)s pt и %(max)s pt", + "Appearance": "Изглед", + "Create room": "Създай стая", + "You've successfully verified your device!": "Успешно потвърдихте устройството си!", + "Message deleted": "Съобщението беше изтрито", + "Message deleted by %(name)s": "Съобщението беше изтрито от %(name)s", + "QR Code": "QR код", + "To continue, use Single Sign On to prove your identity.": "За да продължите, използвайте Single Sign On за да потвърдите самоличността си.", + "Confirm to continue": "Потвърдете за да продължите", + "Click the button below to confirm your identity.": "Кликнете бутона по-долу за да потвърдите самоличността си.", + "a new master key signature": "нов подпис на основния ключ", + "a new cross-signing key signature": "нов подпис на ключа за кръстосано-подписване", + "a device cross-signing signature": "подпис за кръстосано-подписване на устройства", + "a key signature": "подпис на ключ", + "Riot encountered an error during upload of:": "Riot срещна проблем при качването на:", + "Upload completed": "Качването завърши", + "Cancelled signature upload": "Отказано качване на подпис", + "Unable to upload": "Неуспешно качване", + "Signature upload success": "Успешно качване на подпис", + "Signature upload failed": "Неуспешно качване на подпис", + "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:": "Потвърдете сесията на този потребител чрез сравняване на следното с техните Потребителски Настройки:", + "If they don't match, the security of your communication may be compromised.": "Ако няма съвпадение, сигурността на комуникацията ви може би е компрометирана.", + "Your account is not secure": "Профилът ви не е защитен", + "Your password": "Паролата ви", + "This session, or the other session": "Тази сесия или другата сесия", + "The internet connection either session is using": "Интернет връзката, която сесиите използват", + "We recommend you change your password and recovery key in Settings immediately": "Препоръчваме веднага да промените паролата и ключа за възстановяване от Настройки", + "New session": "Нова сесия", + "Use this session to verify your new one, granting it access to encrypted messages:": "Използвайте тази сесия за да потвърдите новата, давайки й достъп до шифрованите съобщения:", + "If you didn’t sign in to this session, your account may be compromised.": "Ако не сте се вписвали в тази сесия, профила ви може би е бил компрометиран.", + "This wasn't me": "Не бях аз", + "This will allow you to return to your account after signing out, and sign in on other sessions.": "Това ще ви позволи да се върнете в профила си след излизането от него, както и да влизате от други сесии.", + "You are currently blacklisting unverified sessions; to send messages to these sessions you must verify them.": "В момента блокирате непотвърдени сесии; за да изпратите съобщения до тях ще трябва да ги потвърдите.", + "We recommend you go through the verification process for each session to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Препоръчваме да минете през процеса на потвърждение за всяка една сесия и да проверите дали принадлежи на собственика си, но ако желаете, може да изпратите съобщението и без потвърждение.", + "Room contains unknown sessions": "Стаята съдържа непознати сесии", + "\"%(RoomName)s\" contains sessions that you haven't seen before.": "\"%(RoomName)s\" съдържа сесии, които не сте виждали досега.", + "Unknown sessions": "Непознати сесии", + "Verify other session": "Потвърди другата сесия", + "Enter recovery passphrase": "Въведете парола за възстановяване", + "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Неуспешен достъп до секретното складиране. Потвърдете, че сте въвели правилната парола за възстановяване.", + "Warning: You should only do this on a trusted computer.": "Внимание: трябва да правите това само от доверен компютър.", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Въведете паролата си за възстановяване за да достъпите защитената история на съобщенията и самоличността си за кръстосано-подписване за потвърждаване на другите сесии.", + "Room name or address": "Име на стая или адрес", + "Joins room with given address": "Присъединява се към стая с дадения адрес", + "Unrecognised room address:": "Неразпознат адрес на стая:", + "Help us improve Riot": "Помогнете ни да подобрим Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Изпращане на анонимни данни за използването, които помагат да се подобри Riot. Това ще използва бисквитка.", + "I want to help": "Искам да помогна", + "Your homeserver has exceeded its user limit.": "Надвишен е лимитът за потребители на сървъра ви.", + "Your homeserver has exceeded one of its resource limits.": "Беше надвишен някой от лимитите на сървъра.", + "Contact your server admin.": "Свържете се със сървърния администратор.", + "Ok": "Добре", + "Set password": "Настрой парола", + "To return to your account in future you need to set a password": "За да се върнете в профила си в бъдеще е необходимо да настройте парола", + "Restart": "Рестартирай", + "Upgrade your Riot": "Обновете Riot", + "A new version of Riot is available!": "Налична е нова версия на Riot!", + "New version available. Update now.": "Налична е нова версия. Обновете сега.", + "Please verify the room ID or address and try again.": "Проверете идентификатора или адреса на стаята и опитайте пак.", + "Room ID or address of ban list": "Идентификатор или адрес на стая списък за блокиране", + "To link to this room, please add an address.": "За да споделите тази стая, добавете адрес.", + "Error creating address": "Неуспешно създаване на адрес", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Възникна грешка при създаването на този адрес. Или не е позволен от сървъра или е временен проблем.", + "You don't have permission to delete the address.": "Нямате права да изтриете адреса.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Възникна грешка при премахването на този адрес. Или не съществува вече или е временен проблем.", + "Error removing address": "Грешка при премахването на адреса", + "Categories": "Категории", + "Room address": "Адрес на стаята", + "Please provide a room address": "Въведете адрес на стаята", + "This address is available to use": "Адресът е наличен за ползване", + "This address is already in use": "Адресът вече се използва", + "Set a room address to easily share your room with other people.": "Настройте адрес на стаята за да може лесно да я споделяте с други хора.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Използвали сте и по-нова версия на Riot от сегашната за тази сесия. За да използвате сегашната версия отново с шифроване от-край-до-край, ще е необходимо да излезете и да влезете отново.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options.": "Ако сте забравили паролата за възстановяване, може да използвате ключа за възстановяване или да настройте нови опции за възстановяване.", + "Enter recovery key": "Въведете ключ за възстановяване", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Достъпете защитената история на съобщенията и самоличността за кръстосано-подписване за верифициране на други сесии, чрез въвеждане на ключа си за възстановяване.", + "Restoring keys from backup": "Възстановяване на ключове от резервно копие", + "Fetching keys from server...": "Изтегляне на ключове от сървъра...", + "%(completed)s of %(total)s keys restored": "%(completed)s от %(total)s ключа са възстановени", + "Recovery key mismatch": "Несъответствие в ключа за възстановяване", + "Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Резервното копие не можа да бъде разшифровано с този ключ за възстановяване: проверете дали сте въвели правилния ключ за възстановяване.", + "Incorrect recovery passphrase": "Неправилна парола за възстановяване", + "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Резервното копие не можа да бъде разшифровано с тази парола за възстановяване: проверете дали сте въвели правилната парола за възстановяване.", + "Keys restored": "Ключовете бяха възстановени", + "Successfully restored %(sessionCount)s keys": "Успешно бяха възстановени %(sessionCount)s ключа", + "Address (optional)": "Адрес (незадължително)", + "Confirm your identity by entering your account password below.": "Потвърдете самоличността си чрез въвеждане на паролата за профила по-долу.", + "Sign in with SSO": "Влезте със SSO", + "Welcome to %(appName)s": "Добре дошли в %(appName)s", + "Liberate your communication": "Освободете комуникацията си", + "Send a Direct Message": "Изпрати директно съобщение", + "Explore Public Rooms": "Разгледай публичните стаи", + "Create a Group Chat": "Създай групов чат", + "Self-verification request": "Заявка за само-потвърждение", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Изтрий %(alias)s адреса на стаята и премахни %(name)s от директорията?", + "delete the address.": "изтриване на адреса.", + "Message not sent due to unknown sessions being present": "Съобщението не е изпратено поради наличието на непознати сесии", + "Show sessions, send anyway or cancel.": "Покажи сесиите, изпрати така или иначе или откажи.", + "Verify this login": "Потвърди тази сесия", + "Session verified": "Сесията беше потвърдена", + "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Промяната на паролата ще нулира всички ключове за шифроване от-край-до-край по всички ваши сесии, правейки шифрованата история на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете на стаите от друга сесия преди да промените паролата си." } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index ddd5e5521b..f25b247d46 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -2150,7 +2150,7 @@ "Sign In or Create Account": "Přihlásit nebo vytvořit nový účet", "Use your account or create a new one to continue.": "Pro pokračování se přihlaste existujícím účtem, nebo si vytvořte nový.", "Create Account": "Vytvořit účet", - "Order rooms by name": "Seřadit místnosti podle názvz", + "Order rooms by name": "Seřadit místnosti podle názvu", "Show rooms with unread notifications first": "Zobrazovat místnosti s nepřečtenými oznámeními navrchu", "Show shortcuts to recently viewed rooms above the room list": "Zobrazovat zkratky do nedávno zobrazených místností navrchu", "Cancelling…": "Rušení…", @@ -2294,5 +2294,34 @@ "Sends a message to the given user": "Pošle zprávu danému uživateli", "Waiting for your other session to verify…": "Čekáme na ověření od vaší druhé relace…", "Verify all your sessions to ensure your account & messages are safe": "Ověřte všechny své relace, abyste zaručili, že jsou vaše zprávy a účet bezpečné", - "Verify the new login accessing your account: %(name)s": "Ověřte nové přihlášení na váš účet: %(name)s" + "Verify the new login accessing your account: %(name)s": "Ověřte nové přihlášení na váš účet: %(name)s", + "Room name or address": "Jméno nebo adresa místnosti", + "Joins room with given address": "Přidat se do místnosti s danou adresou", + "Unrecognised room address:": "Nerozpoznaná adresa místnosti:", + "Use IRC layout": "Používat rozložení IRC", + "Font size": "Velikost písma", + "Custom font size": "Vlastní velikost písma", + "IRC display name width": "šířka zobrazovného IRC jména", + "unexpected type": "neočekávaný typ", + "Session backup key:": "Klíč k záloze relace:", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Pomocí Jednotného přihlášení potvrdit odstranění těchto relací.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Pomocí Jednotného přihlášení potvrdit odstranění této relace.", + "Size must be a number": "Velikost musí být číslo", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Vlastní velikost písma může být pouze mezi %(min)s pt a %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Použijte velikost mezi %(min)s pt a %(max)s pt", + "Appearance": "Vzhled", + "Please verify the room ID or address and try again.": "Ověřte prosím, že ID místnosti je správné a zkuste to znovu.", + "Room ID or address of ban list": "ID nebo adresa seznamu zablokovaných", + "Help us improve Riot": "Pomozte nám zlepšovat Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Zasílat anonymní data o použití aplikace, která nám pomáhají Riot zlepšovat. Bedeme na to používat soubory cookie.", + "I want to help": "Chci pomoci", + "Your homeserver has exceeded its user limit.": "Na vašem domovském serveru byl překročen limit počtu uživatelů.", + "Your homeserver has exceeded one of its resource limits.": "Na vašem domovském serveru byl překročen limit systémových požadavků.", + "Contact your server admin.": "Kontaktujte administrátora serveru.", + "Ok": "Ok", + "Set password": "Nastavit heslo", + "To return to your account in future you need to set a password": "Abyste se k účtu mohli v budoucnu vrátit, je potřeba nastavit heslo", + "Restart": "Restartovat", + "Upgrade your Riot": "Aktualizovat Riot", + "A new version of Riot is available!": "Je dostupná nová verze Riotu!" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 1166e7710a..f55d94f8fc 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1103,7 +1103,7 @@ "Room information": "Rauminformationen", "Internal room ID:": "Interne Raum ID:", "Room version": "Raum Version", - "Room version:": "Raum Version:", + "Room version:": "Raum-Version:", "Developer options": "Entwickleroptionen", "General": "Allgemein", "Set a new account password...": "Neues Benutzerkonto-Passwort festlegen...", @@ -1543,7 +1543,7 @@ "View rules": "Regeln betrachten", "You are currently subscribed to:": "Du abonnierst momentan:", "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.", - "The version of Riot": "Die Version von Riot", + "The version of Riot": "Die Riot-Version", "Whether you're using Riot on a device where touch is the primary input mechanism": "Ob du Riot auf einem Gerät verwendest, bei dem Berührung der primäre Eingabemechanismus ist", "Whether you're using Riot as an installed Progressive Web App": "Ob Sie Riot als installierte progressive Web-App verwenden", "Your user agent": "Dein User-Agent", @@ -1793,15 +1793,15 @@ "Use Single Sign On to continue": "Benutze „Single Sign-On“ (Einmalanmeldung) um fortzufahren", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse mit „Single Sign-On“, um deine Identität nachzuweisen.", "Single Sign On": "Single Sign-On", - "Confirm adding email": "Bestätige das Hinzfugen der Email-Addresse", + "Confirm adding email": "Bestätige das Hinzufügen der Email-Adresse", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser Telefonnummer, indem du deine Identität mittels „Single Sign-On“ nachweist.", - "Click the button below to confirm adding this phone number.": "Betätige unten die Schaltfläche um das Hinzufügen dieser Telefonnummer zu bestätigen.", + "Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.", "If you cancel now, you won't complete your operation.": "Wenn du jetzt abbrichst, wirst du deinen Vorgang nicht fertigstellen.", "%(name)s is requesting verification": "%(name)s fordert eine Verifizierung an", "Failed to set topic": "Das Festlegen des Themas ist fehlgeschlagen", "Command failed": "Befehl fehlgeschlagen", "Could not find user in room": "Der Benutzer konnte im Raum nicht gefunden werden", - "Click the button below to confirm adding this email address.": "Klicken Sie auf die Schaltfläche unten, um das Hinzufügen dieser E-Mail-Adresse zu bestätigen.", + "Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.", "Confirm adding phone number": "Bestätige das Hinzufügen der Telefonnummer", "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s erneuert eine Ausschluss-Regel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s", @@ -2375,5 +2375,19 @@ "Confirm to continue": "Bestätige um fortzufahren", "Click the button below to confirm your identity.": "Klicke den Button unten um deine Identität zu bestätigen.", "Confirm encryption setup": "Bestätige die Einrichtung der Verschlüsselung", - "Click the button below to confirm setting up encryption.": "Klick die Schaltfläche unten um die Einstellungen der Verschlüsselung zu bestätigen." + "Click the button below to confirm setting up encryption.": "Klick die Schaltfläche unten um die Einstellungen der Verschlüsselung zu bestätigen.", + "Font scaling": "Schriftskalierung", + "Use the improved room list (in development - refresh to apply changes)": "Verwende die verbesserte Raumliste (in Entwicklung - neu laden um die Änderungen anzuwenden)", + "Use IRC layout": "Verwende das IRC Layout", + "Font size": "Schriftgröße", + "Custom font size": "Eigene Schriftgröße", + "IRC display name width": "Breite des IRC Anzeigenamens", + "Size must be a number": "Größe muss eine Zahl sein", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein", + "Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt", + "Appearance": "Erscheinungsbild", + "Create room": "Raum erstellen", + "Jump to oldest unread message": "Zur ältesten ungelesenen Nachricht springen", + "Upload a file": "Eine Datei hochladen", + "Dismiss read marker and jump to bottom": "Entferne Lesemarker und springe nach unten" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 96ccf1589d..0aa4c3779e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -35,12 +35,6 @@ "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", "Call Failed": "Call Failed", - "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.", - "Review Sessions": "Review Sessions", - "Call Anyway": "Call Anyway", - "Answer Anyway": "Answer Anyway", - "Call": "Call", - "Answer": "Answer", "Call Timeout": "Call Timeout", "The remote side failed to pick up": "The remote side failed to pick up", "Call failed due to misconfigured server": "Call failed due to misconfigured server", @@ -75,8 +69,6 @@ "Enter passphrase": "Enter passphrase", "Cancel": "Cancel", "Setting up keys": "Setting up keys", - "Send anyway": "Send anyway", - "Send": "Send", "Sun": "Sun", "Mon": "Mon", "Tue": "Tue", @@ -102,11 +94,6 @@ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", - "Verify this session": "Verify this session", - "Encryption upgrade available": "Encryption upgrade available", - "Set up encryption": "Set up encryption", - "Review where you’re logged in": "Review where you’re logged in", - "New login. Was this you?": "New login. Was this you?", "Who would you like to add to this community?": "Who would you like to add to this community?", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID", "Invite new community members": "Invite new community members", @@ -115,7 +102,7 @@ "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?", "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?", "Add rooms to the community": "Add rooms to the community", - "Room name or alias": "Room name or alias", + "Room name or address": "Room name or address", "Add to community": "Add to community", "Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:", "Failed to invite users to community": "Failed to invite users to community", @@ -185,9 +172,9 @@ "Use an identity server": "Use an identity server", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", - "Joins room with given alias": "Joins room with given alias", + "Joins room with given address": "Joins room with given address", "Leave room": "Leave room", - "Unrecognised room alias:": "Unrecognised room alias:", + "Unrecognised room address:": "Unrecognised room address:", "Kicks user with given id": "Kicks user with given id", "Bans user with given id": "Bans user with given id", "Unbans user with given ID": "Unbans user with given ID", @@ -396,6 +383,42 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Help us improve Riot": "Help us improve Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Send anonymous usage data which helps us improve Riot. This will use a cookie.", + "I want to help": "I want to help", + "No": "No", + "Review where you’re logged in": "Review where you’re logged in", + "Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe", + "Review": "Review", + "Later": "Later", + "Notifications": "Notifications", + "You are not receiving desktop notifications": "You are not receiving desktop notifications", + "Enable them now": "Enable them now", + "Close": "Close", + "Your homeserver has exceeded its user limit.": "Your homeserver has exceeded its user limit.", + "Your homeserver has exceeded one of its resource limits.": "Your homeserver has exceeded one of its resource limits.", + "Contact your server admin.": "Contact your server admin.", + "Warning": "Warning", + "Ok": "Ok", + "Set password": "Set password", + "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", + "Set Password": "Set Password", + "Set up encryption": "Set up encryption", + "Encryption upgrade available": "Encryption upgrade available", + "Verify this session": "Verify this session", + "Set up": "Set up", + "Upgrade": "Upgrade", + "Verify": "Verify", + "Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe", + "Other users may not trust it": "Other users may not trust it", + "New login. Was this you?": "New login. Was this you?", + "Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s", + "What's new?": "What's new?", + "What's New": "What's New", + "Update": "Update", + "Restart": "Restart", + "Upgrade your Riot": "Upgrade your Riot", + "A new version of Riot is available!": "A new version of Riot is available!", "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", @@ -410,7 +433,6 @@ "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Use IRC layout": "Use IRC layout", - "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", "Custom font size": "Custom font size", @@ -570,15 +592,6 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", - "Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe", - "Later": "Later", - "Review": "Review", - "Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe", - "Other users may not trust it": "Other users may not trust it", - "Set up": "Set up", - "Upgrade": "Upgrade", - "Verify": "Verify", - "Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s", "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", @@ -643,8 +656,6 @@ "Last seen": "Last seen", "Failed to set display name": "Failed to set display name", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", - "Disable Notifications": "Disable Notifications", - "Enable Notifications": "Enable Notifications", "Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ", " to store messages from ": " to store messages from ", "rooms.": "rooms.", @@ -680,7 +691,6 @@ "Backup has an invalid signature from unverified session ": "Backup has an invalid signature from unverified session ", "Backup is not signed by any of your sessions": "Backup is not signed by any of your sessions", "This backup is trusted because it has been restored on this session": "This backup is trusted because it has been restored on this session", - "Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", "Backup key stored: ": "Backup key stored: ", @@ -752,16 +762,22 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", + "Checking for an update...": "Checking for an update...", + "No update available.": "No update available.", + "Downloading update...": "Downloading update...", + "New version available. Update now.": "New version available. Update now.", + "Check for update": "Check for update", "Size must be a number": "Size must be a number", "Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt", "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", "Invalid theme schema.": "Invalid theme schema.", "Error downloading theme information.": "Error downloading theme information.", "Theme added!": "Theme added!", - "Appearance": "Appearance", "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", + "Appearance": "Appearance", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", @@ -776,7 +792,6 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", - "Warning": "Warning", "General": "General", "Discovery": "Discovery", "Deactivate account": "Deactivate account", @@ -785,7 +800,6 @@ "For help with using Riot, click here.": "For help with using Riot, click here.", "For help with using Riot, click here or start a chat with our bot using the button below.": "For help with using Riot, click here or start a chat with our bot using the button below.", "Chat with Riot Bot": "Chat with Riot Bot", - "Check for update": "Check for update", "Help & About": "Help & About", "Bug reporting": "Bug reporting", "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited 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. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", @@ -807,7 +821,7 @@ "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", "Error subscribing to list": "Error subscribing to list", - "Please verify the room ID or alias and try again.": "Please verify the room ID or alias and try again.", + "Please verify the room ID or address and try again.": "Please verify the room ID or address and try again.", "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", @@ -815,7 +829,6 @@ "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", - "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "You are currently ignoring:": "You are currently ignoring:", "You are not subscribed to any lists": "You are not subscribed to any lists", @@ -834,9 +847,8 @@ "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", - "Room ID or alias of ban list": "Room ID or alias of ban list", + "Room ID or address of ban list": "Room ID or address of ban list", "Subscribe": "Subscribe", - "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", @@ -859,6 +871,7 @@ "Key backup": "Key backup", "Message search": "Message search", "Cross-signing": "Cross-signing", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", "Security & Privacy": "Security & Privacy", "Where you’re logged in": "Where you’re logged in", "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Manage the names of and sign out of your sessions below or verify them in your User Profile.", @@ -936,7 +949,7 @@ "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "Click here to fix": "Click here to fix", - "To link to this room, please add an alias.": "To link to this room, please add an alias.", + "To link to this room, please add an address.": "To link to this room, please add an address.", "Only people who have been invited": "Only people who have been invited", "Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests", "Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests", @@ -992,10 +1005,6 @@ "Someone is using an unknown session": "Someone is using an unknown session", "This room is end-to-end encrypted": "This room is end-to-end encrypted", "Everyone in this room is verified": "Everyone in this room is verified", - "Some sessions for this user are not trusted": "Some sessions for this user are not trusted", - "All sessions for this user are trusted": "All sessions for this user are trusted", - "Some sessions in this encrypted room are not trusted": "Some sessions in this encrypted room are not trusted", - "All sessions in this encrypted room are trusted": "All sessions in this encrypted room are trusted", "Edit message": "Edit message", "Mod": "Mod", "This event could not be displayed": "This event could not be displayed", @@ -1015,51 +1024,6 @@ "Invite only": "Invite only", "Scroll to most recent messages": "Scroll to most recent messages", "Close preview": "Close preview", - "device id: ": "device id: ", - "Disinvite": "Disinvite", - "Kick": "Kick", - "Disinvite this user?": "Disinvite this user?", - "Kick this user?": "Kick this user?", - "Failed to kick": "Failed to kick", - "Ban": "Ban", - "Unban this user?": "Unban this user?", - "Ban this user?": "Ban this user?", - "Failed to ban user": "Failed to ban user", - "No recent messages by %(user)s found": "No recent messages by %(user)s found", - "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", - "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "You are about to remove 1 message by %(user)s. This cannot be undone. Do you wish to continue?", - "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", - "Remove %(count)s messages|other": "Remove %(count)s messages", - "Remove %(count)s messages|one": "Remove 1 message", - "Demote yourself?": "Demote yourself?", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", - "Demote": "Demote", - "Failed to mute user": "Failed to mute user", - "Failed to toggle moderator status": "Failed to toggle moderator status", - "Deactivate user?": "Deactivate user?", - "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", - "Deactivate user": "Deactivate user", - "Failed to deactivate user": "Failed to deactivate user", - "Failed to change power level": "Failed to change power level", - "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", - "Are you sure?": "Are you sure?", - "No sessions with registered encryption keys": "No sessions with registered encryption keys", - "Sessions": "Sessions", - "Jump to read receipt": "Jump to read receipt", - "Mention": "Mention", - "Invite": "Invite", - "Share Link to User": "Share Link to User", - "User Options": "User Options", - "Start a chat": "Start a chat", - "Direct chats": "Direct chats", - "Remove recent messages": "Remove recent messages", - "Unmute": "Unmute", - "Mute": "Mute", - "Revoke Moderator": "Revoke Moderator", - "Make Moderator": "Make Moderator", - "Admin Tools": "Admin Tools", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", @@ -1069,13 +1033,12 @@ "Voice call": "Voice call", "Video call": "Video call", "Hangup": "Hangup", + "Emoji picker": "Emoji picker", "Upload file": "Upload file", "Send an encrypted reply…": "Send an encrypted reply…", "Send a reply…": "Send a reply…", "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", - "Send a reply (unencrypted)…": "Send a reply (unencrypted)…", - "Send a message (unencrypted)…": "Send a message (unencrypted)…", "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", @@ -1173,6 +1136,7 @@ "Jump to first unread room.": "Jump to first unread room.", "Jump to first invite.": "Jump to first invite.", "Add room": "Add room", + "Show %(n)s more": "Show %(n)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.", @@ -1204,6 +1168,7 @@ "Show Stickers": "Show Stickers", "Failed to revoke invite": "Failed to revoke invite", "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.", + "Admin Tools": "Admin Tools", "Revoke invite": "Revoke invite", "Invited by %(sender)s": "Invited by %(sender)s", "Jump to first unread message.": "Jump to first unread message.", @@ -1211,11 +1176,11 @@ "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", - "Error creating alias": "Error creating alias", - "There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.": "There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.", - "You don't have permission to delete the alias.": "You don't have permission to delete the alias.", - "There was an error removing that alias. It may no longer exist or a temporary error occurred.": "There was an error removing that alias. It may no longer exist or a temporary error occurred.", - "Error removing alias": "Error removing alias", + "Error creating address": "Error creating address", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.", + "You don't have permission to delete the address.": "You don't have permission to delete the address.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "There was an error removing that address. It may no longer exist or a temporary error occurred.", + "Error removing address": "Error removing address", "Main address": "Main address", "not specified": "not specified", "This room has no local addresses": "This room has no local addresses", @@ -1270,13 +1235,48 @@ "%(count)s sessions|other": "%(count)s sessions", "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", + "Jump to read receipt": "Jump to read receipt", + "Mention": "Mention", + "Invite": "Invite", + "Share Link to User": "Share Link to User", "Direct message": "Direct message", + "Demote yourself?": "Demote yourself?", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", + "Demote": "Demote", + "Disinvite": "Disinvite", + "Kick": "Kick", + "Disinvite this user?": "Disinvite this user?", + "Kick this user?": "Kick this user?", + "Failed to kick": "Failed to kick", + "No recent messages by %(user)s found": "No recent messages by %(user)s found", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", + "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "You are about to remove 1 message by %(user)s. This cannot be undone. Do you wish to continue?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "Remove %(count)s messages|other": "Remove %(count)s messages", + "Remove %(count)s messages|one": "Remove 1 message", + "Remove recent messages": "Remove recent messages", + "Ban": "Ban", + "Unban this user?": "Unban this user?", + "Ban this user?": "Ban this user?", + "Failed to ban user": "Failed to ban user", + "Failed to mute user": "Failed to mute user", + "Unmute": "Unmute", + "Mute": "Mute", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Failed to change power level": "Failed to change power level", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", + "Are you sure?": "Are you sure?", + "Deactivate user?": "Deactivate user?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", + "Deactivate user": "Deactivate user", + "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Security": "Security", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.", @@ -1287,7 +1287,6 @@ "Verify by emoji": "Verify by emoji", "Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?", "Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?", - "No": "No", "Yes": "Yes", "Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.", "In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.", @@ -1381,24 +1380,6 @@ "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "You're not currently a member of any communities.": "You're not currently a member of any communities.", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.", - "Yes, I want to help!": "Yes, I want to help!", - "You are not receiving desktop notifications": "You are not receiving desktop notifications", - "Enable them now": "Enable them now", - "What's New": "What's New", - "Update": "Update", - "What's new?": "What's new?", - "A new version of Riot is available.": "A new version of Riot is available.", - "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", - "Set Password": "Set Password", - "Please contact your service administrator to get this limit increased.": "Please contact your service administrator to get this limit increased.", - "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.", - "This homeserver has exceeded one of its resource limits so some users will not be able to log in.": "This homeserver has exceeded one of its resource limits so some users will not be able to log in.", - "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", - "Checking for an update...": "Checking for an update...", - "No update available.": "No update available.", - "Downloading update...": "Downloading update...", "Frequently Used": "Frequently Used", "Smileys & People": "Smileys & People", "Animals & Nature": "Animals & Nature", @@ -1408,6 +1389,7 @@ "Objects": "Objects", "Symbols": "Symbols", "Flags": "Flags", + "Categories": "Categories", "Quick Reactions": "Quick Reactions", "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", @@ -1434,16 +1416,11 @@ "Popout widget": "Popout widget", "More options": "More options", "Create new room": "Create new room", - "Unblacklist": "Unblacklist", - "Blacklist": "Blacklist", - "Unverify": "Unverify", - "Verify...": "Verify...", "Join": "Join", "No results": "No results", "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", "collapse": "collapse", "expand": "expand", - "Communities": "Communities", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Rotate Left": "Rotate Left", @@ -1511,12 +1488,12 @@ "QR Code": "QR Code", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", "In reply to ": "In reply to ", - "Room alias": "Room alias", + "Room address": "Room address", "e.g. my-room": "e.g. my-room", "Some characters not allowed": "Some characters not allowed", - "Please provide a room alias": "Please provide a room alias", - "This alias is available to use": "This alias is available to use", - "This alias is already in use": "This alias is already in use", + "Please provide a room address": "Please provide a room address", + "This address is available to use": "This address is available to use", + "This address is already in use": "This address is already in use", "Room directory": "Room directory", "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", @@ -1582,10 +1559,10 @@ "example": "example", "Create": "Create", "Please enter a name for the room": "Please enter a name for the room", - "Set a room alias to easily share your room with other people.": "Set a room alias to easily share your room with other people.", + "Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.", "This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.", - "Enable end-to-end encryption": "Enable end-to-end encryption", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", + "Enable end-to-end encryption": "Enable end-to-end encryption", "Create a public room": "Create a public room", "Create a private room": "Create a private room", "Name": "Name", @@ -1597,7 +1574,7 @@ "Create Room": "Create Room", "Sign out": "Sign out", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "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 Riot to do this", - "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.", @@ -1611,22 +1588,8 @@ "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.", "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)", - "Verify session": "Verify session", - "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", - "Verify by comparing a short text string.": "Verify by comparing a short text string.", - "Begin Verifying": "Begin Verifying", - "Waiting for partner to accept...": "Waiting for partner to accept...", - "Nothing appearing? Not all clients support interactive verification yet. .": "Nothing appearing? Not all clients support interactive verification yet. .", - "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", - "To verify that this session can be trusted, please check that the key you see in User Settings on that device matches the key below:": "To verify that this session can be trusted, please check that the key you see in User Settings on that device matches the key below:", - "To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:": "To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:", - "Use two-way text verification": "Use two-way text verification", - "Session name": "Session name", - "Session ID": "Session ID", - "Session key": "Session key", - "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this session and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this session and you probably want to press the blacklist button instead.", - "I verify that the keys match": "I verify that the keys match", "Back": "Back", + "Send": "Send", "Send Custom Event": "Send Custom Event", "You must specify an event type!": "You must specify an event type!", "Event sent!": "Event sent!", @@ -1668,13 +1631,6 @@ "Start a conversation with someone using their name, username (like ) or email address.": "Start a conversation with someone using their name, username (like ) or email address.", "Go": "Go", "Invite someone using their name, username (like ), email address or share this room.": "Invite someone using their name, username (like ), email address or share this room.", - "You added a new session '%(displayName)s', which is requesting encryption keys.": "You added a new session '%(displayName)s', which is requesting encryption keys.", - "Your unverified session '%(displayName)s' is requesting encryption keys.": "Your unverified session '%(displayName)s' is requesting encryption keys.", - "Start verification": "Start verification", - "Share without verifying": "Share without verifying", - "Ignore request": "Ignore request", - "Loading session info...": "Loading session info...", - "Encryption key request": "Encryption key request", "a new master key signature": "a new master key signature", "a new cross-signing key signature": "a new cross-signing key signature", "a device cross-signing signature": "a device cross-signing signature", @@ -1697,7 +1653,11 @@ "Are you sure you want to sign out?": "Are you sure you want to sign out?", "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", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", "Message edits": "Message edits", "Your account is not secure": "Your account is not secure", @@ -1783,11 +1743,6 @@ "Summary": "Summary", "Document": "Document", "Next": "Next", - "You are currently blacklisting unverified sessions; to send messages to these sessions you must verify them.": "You are currently blacklisting unverified sessions; to send messages to these sessions you must verify them.", - "We recommend you go through the verification process for each session to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each session to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", - "Room contains unknown sessions": "Room contains unknown sessions", - "\"%(RoomName)s\" contains sessions that you haven't seen before.": "\"%(RoomName)s\" contains sessions that you haven't seen before.", - "Unknown sessions": "Unknown sessions", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -1838,7 +1793,7 @@ "Private Chat": "Private Chat", "Public Chat": "Public Chat", "Custom": "Custom", - "Alias (optional)": "Alias (optional)", + "Address (optional)": "Address (optional)", "Reject invitation": "Reject invitation", "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?", "Unable to reject invite": "Unable to reject invite", @@ -1855,7 +1810,6 @@ "Share Message": "Share Message", "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", - "End-to-end encryption information": "End-to-end encryption information", "Report Content": "Report Content", "Failed to set Direct Message status of room": "Failed to set Direct Message status of room", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", @@ -2027,17 +1981,18 @@ "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", + "Communities": "Communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "You have no visible notifications": "You have no visible notifications", "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "Riot failed to get the public room list.": "Riot failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", - "Delete the room alias %(alias)s and remove %(name)s from the directory?": "Delete the room alias %(alias)s and remove %(name)s from the directory?", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Delete the room address %(alias)s and remove %(name)s from the directory?", "Remove %(name)s from the directory?": "Remove %(name)s from the directory?", "Remove from Directory": "Remove from Directory", "remove %(name)s from the directory.": "remove %(name)s from the directory.", - "delete the alias.": "delete the alias.", + "delete the address.": "delete the address.", "The server may be unavailable or overloaded": "The server may be unavailable or overloaded", "Unable to join network": "Unable to join network", "Riot does not know how to join a room on this network": "Riot does not know how to join a room on this network", @@ -2051,8 +2006,6 @@ "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", "Explore rooms": "Explore rooms", - "Message not sent due to unknown sessions being present": "Message not sent due to unknown sessions being present", - "Show sessions, send anyway or cancel.": "Show sessions, send anyway or cancel.", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", @@ -2083,7 +2036,6 @@ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", - " (1/%(totalCount)s)": " (1/%(totalCount)s)", "Guest": "Guest", "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", @@ -2176,22 +2128,6 @@ "Room Autocomplete": "Room Autocomplete", "Users": "Users", "User Autocomplete": "User Autocomplete", - "unknown device": "unknown device", - "NOT verified": "NOT verified", - "Blacklisted": "Blacklisted", - "verified": "verified", - "Device ID": "Device ID", - "Verification": "Verification", - "Ed25519 fingerprint": "Ed25519 fingerprint", - "User ID": "User ID", - "Curve25519 identity key": "Curve25519 identity key", - "none": "none", - "Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key", - "Algorithm": "Algorithm", - "unencrypted": "unencrypted", - "Decryption error": "Decryption error", - "Event information": "Event information", - "Sender session information": "Sender session information", "Passphrases must match": "Passphrases must match", "Passphrase must not be empty": "Passphrase must not be empty", "Export room keys": "Export room keys", @@ -2217,6 +2153,7 @@ "Back up encrypted message keys": "Back up encrypted message keys", "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", + "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 3f749ebe0f..5252deba23 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -385,7 +385,7 @@ "Communities": "Komunumoj", "Home": "Hejmo", "Could not connect to the integration server": "Malsukcesis konektiĝi al la kuniga servilo", - "Manage Integrations": "Administri integrojn", + "Manage Integrations": "Administri kunigojn", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s-foje aliĝis", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)saliĝis", @@ -882,7 +882,7 @@ "Delete Backup": "Forigi savkopion", "Language and region": "Lingvo kaj regiono", "Theme": "Haŭto", - "General": "Ĝenerala", + "General": "Ĝeneralaj", "In reply to ": "Responde al ", "Share Message": "Diskonigi", "Whether or not you're logged in (we don't record your username)": "Ĉu vi salutis aŭ ne (ni ne registras vian uzantonomon)", @@ -974,9 +974,9 @@ "Room list": "Ĉambrolisto", "Ignored users": "Malatentaj uzantoj", "Key backup": "Sekurkopio de ŝlosilo", - "Security & Privacy": "Sekureco & Privateco", + "Security & Privacy": "Sekureco kaj Privateco", "Voice & Video": "Voĉo kaj vido", - "Room information": "Ĉambraj informoj", + "Room information": "Informoj pri ĉambro", "Internal room ID:": "Ena ĉambra identigilo:", "Room version": "Ĉambra versio", "Room version:": "Ĉambra versio:", @@ -998,7 +998,7 @@ "Remove messages": "Forigi mesaĝojn", "Notify everyone": "Sciigi ĉiujn", "Muted Users": "Silentigitaj uzantoj", - "Roles & Permissions": "Roloj & Permesoj", + "Roles & Permissions": "Roloj kaj Permesoj", "Enable encryption?": "Ĉu ŝalti ĉifradon?", "Share Link to User": "Kunhavigi ligilon al uzanto", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Vidita de %(displayName)s (%(userName)s) je %(dateTime)s", @@ -1017,7 +1017,7 @@ "To continue, please enter your password:": "Por daŭrigi, bonvoluenigi vian pasvorton:", "Updating Riot": "Ĝisdatigante Riot", "Go back": "Reen iri", - "Room Settings - %(roomName)s": "Ĉambraj agordoj — %(roomName)s", + "Room Settings - %(roomName)s": "Agordoj de ĉambro – %(roomName)s", "Failed to upgrade room": "Malsukcesis gradaltigi ĉambron", "Refresh": "Aktualigi", "Checking...": "Kontrolante…", @@ -1025,7 +1025,7 @@ "Share User": "Kunhavigi uzanton", "Share Community": "Kunhavigi komunumon", "Share Room Message": "Kunhavigi ĉambran mesaĝon", - "COPY": "KOPIO", + "COPY": "KOPII", "Next": "Sekva", "Clear status": "Vakigi staton", "Update status": "Ĝisdatigi staton", @@ -1386,7 +1386,7 @@ "The conversation continues here.": "La interparolo pluas ĉi tie.", "This room has been replaced and is no longer active.": "Ĉi tiu ĉambro estas anstataŭita, kaj ne plu aktivas.", "Loading room preview": "Preparas antaŭrigardon al la ĉambro", - "Only room administrators will see this warning": "Nur ĉambraj administrantoj vidos ĉi tiun averton", + "Only room administrators will see this warning": "Nur administrantoj de ĉambro vidos ĉi tiun averton", "Error updating flair": "Eraris ĝisdatigo de etikedo", "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Eraris ĝisdatigo de etikedo por ĉi tiu ĉambro. Aŭ la servilo ne permesas ĝin, aŭ dumtempa eraro okazis.", "Showing flair for these communities:": "Montras etikedojn de la jenaj komunumoj:", @@ -2055,7 +2055,7 @@ "%(displayName)s cancelled verification. Start verification again from their profile.": "%(displayName)s nuligis la kontrolon. Rekomencu ĝin de ĝia profilo.", "You cancelled verification. Start verification again from their profile.": "Vi nuligis la kontrolon. Rekomencu ĝin de ĝia profilo.", "Encryption enabled": "Ĉifrado estas ŝaltita", - "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Mesaĝojn en ĉi tiu ĉambro estas tutvoje ĉifrataj. Eksciu plion kaj kontrolu ĉi tiun uzanton el ĝia profilo.", + "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Mesaĝoj en ĉi tiu ĉambro estas tutvoje ĉifrataj. Eksciu plion kaj kontrolu ĉi tiun uzanton per ĝia profilo.", "Encryption not enabled": "Ĉifrado ne estas ŝaltita", "The encryption used by this room isn't supported.": "La ĉifro uzata de ĉi tiu ĉambro ne estas subtenata.", "You have ignored this user, so their message is hidden. Show anyways.": "Vi malatentis ĉi tiun uzanton, ĝia mesaĝo estas do kaŝita. Tamen montri.", @@ -2409,5 +2409,51 @@ "QR Code": "Rapidresponda kodo", "Dismiss read marker and jump to bottom": "Forigi legomarkon kaj iri al fundo", "Jump to oldest unread message": "Iri al plej malnova nelegita mesaĝo", - "Upload a file": "Alŝuti dosieron" + "Upload a file": "Alŝuti dosieron", + "Create room": "Krei ĉambron", + "Use IRC layout": "Uzi aranĝon de IRC", + "IRC display name width": "Larĝo de vidiga nomo de IRC", + "Font scaling": "Skalado de tiparoj", + "Font size": "Grando de tiparo", + "Custom font size": "Propra grando de tiparo", + "Size must be a number": "Grando devas esti nombro", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Propra grando de tiparo povas interi nur %(min)s punktojn kaj %(max)s punktojn", + "Use between %(min)s pt and %(max)s pt": "Uzi inter %(min)s punktoj kaj %(max)s punktoj", + "Appearance": "Aspekto", + "Use the improved room list (in development - refresh to apply changes)": "Uzi la plibonigitan liston de ĉambroj (ankoraŭ evoluigate – aktualigu la paĝon por efektivigi ŝanĝojn)", + "Room name or address": "Nomo aŭ adreso de ĉambro", + "Joins room with given address": "Aligas al ĉambro kun donita adreso", + "Unrecognised room address:": "Nerekonita adreso de ĉambro:", + "Please verify the room ID or address and try again.": "Bonvolu kontroli identigilon aŭ adreson de la ĉambro kaj reprovi.", + "Room ID or address of ban list": "Ĉambra identigilo aŭ adreso de listo de forbaroj", + "To link to this room, please add an address.": "Por ligi al ĉi tiu ĉambro, bonvolu aldoni adreson.", + "Error creating address": "Eraris kreado de adreso", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Eraris kreado de tiu adreso. Eble ĝi ne estas permesata de la servilo, aŭ okazis portempa fiasko.", + "You don't have permission to delete the address.": "Vi ne rajtas forigi la adreson.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Eraris forigo de la adreso. Eble ĝi ne plu ekzistas, aŭ okazis portempa eraro.", + "Error removing address": "Eraris forigo de adreso", + "Categories": "Kategorioj", + "Room address": "Adreso de ĉambro", + "Please provide a room address": "Bonvolu doni adreson de ĉambro", + "This address is available to use": "Ĉi tiu adreso estas uzebla", + "This address is already in use": "Ĉi tiu adreso jam estas uzata", + "Set a room address to easily share your room with other people.": "Agordu adreson de ĉambro por facile konigi la ĉambron al aliuloj.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Vi antaŭe uzis pli novan version de Riot kun tiu ĉi salutaĵo. Por ree uzi ĉi tiun version kun tutvoja ĉifrado, vi devos adiaŭi kaj resaluti.", + "Address (optional)": "Adreso (malnepra)", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Ĉu forigi la adreson de ĉambro %(alias)s kaj forigi %(name)s de la listo?", + "delete the address.": "forigi la adreson.", + "Use a different passphrase?": "Ĉu uzi alian pasfrazon?", + "Help us improve Riot": "Helpu al ni plibonigi Rioton", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Sendi sennomajn datumojn pri uzado, kiuj helpos al ni plibonigi Rioton. Ĉi tio uzos kuketon.", + "I want to help": "Mi volas helpi", + "Your homeserver has exceeded its user limit.": "Via hejmservilo atingis sian limon de uzantoj.", + "Your homeserver has exceeded one of its resource limits.": "Via hejmservilo atingis iun limon de rimedoj.", + "Contact your server admin.": "Kontaktu administranton de via servilo.", + "Ok": "Bone", + "Set password": "Agordi pasvorton", + "To return to your account in future you need to set a password": "Por reveni ose al via konto, vi devas agordi pasvorton", + "Restart": "Restartigi", + "Upgrade your Riot": "Gradaltigi vian Rioton", + "A new version of Riot is available!": "Nova versio de Riot estas disponebla!", + "New version available. Update now.": "Nova versio estas disponebla. Ĝisdatigu nun." } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 3fb069055c..d00525b420 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1236,5 +1236,45 @@ "Nothing appearing? Not all clients support interactive verification yet. .": "Mitte midagi ei kuvata? Kõik Matrix'i kliendid ei toeta veel interaktiivset verifitseerimist. .", "Waiting for %(userId)s to confirm...": "Ootan kinnitust kasutajalt %(userId)s…", "Skip": "Jäta vahele", - "Token incorrect": "Vigane tunnusluba" + "Token incorrect": "Vigane tunnusluba", + "%(oneUser)schanged their name %(count)s times|one": "Kasutaja %(oneUser)s muutis oma nime", + "Are you sure you want to deactivate your account? This is irreversible.": "Kas sa oled kindel, et soovid oma konto sulgeda? Seda tegevust ei saa hiljem tagasi pöörata.", + "Confirm account deactivation": "Kinnita konto sulgemine", + "There was a problem communicating with the server. Please try again.": "Serveriühenduses tekkis viga. Palun proovi uuesti.", + "Server did not return valid authentication information.": "Serveri saadetud vastuses ei olnud kehtivat autentimisteavet.", + "Please fill why you're reporting.": "Palun kirjelda veateate põhjust.", + "Something went wrong trying to invite the users.": "Kasutajatele kutse saatmisel läks midagi viltu.", + "We couldn't invite those users. Please check the users you want to invite and try again.": "Meil ei õnnestunud neile kasutajatele kutset saata. Palun kontrolli, keda soovid kutsuda ning proovi uuesti.", + "Failed to find the following users": "Järgnevaid kasutajaid ei õnnestunud leida", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Järgmisi kasutajanimesid pole olemas või on vigaselt kirjas ning seega ei saa neile kutset saata: %(csvNames)s", + "Recently Direct Messaged": "Viimased otsesõnumite saajad", + "Invite someone using their name, username (like ), email address or share this room.": "Kutsu kedagi tema nime, kasutajanime (nagu ), e-posti aadressi alusel või jaga seda jututuba.", + "You added a new session '%(displayName)s', which is requesting encryption keys.": "Sa oled lisanud uue sessiooni '%(displayName)s', mis küsib krüptimisvõtmeid.", + "Start verification": "Alusta verifitseerimist", + "Share without verifying": "Jaga ilma verifitseerimata", + "Loading session info...": "Laen sessiooniteavet…", + "Encryption key request": "Krüptimisvõtmete päring", + "Upload completed": "Üleslaadimine valmis", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot kasutab varasemaga võrreldes 3-5 korda vähem mälu, sest laeb teavet kasutajate kohta vaid siis, kui vaja. Palun oota hetke, kuni sünkroniseerime andmeid serveriga!", + "Updating Riot": "Uuenda Riot'it", + "I don't want my encrypted messages": "Ma ei soovi oma krüptitud sõnumeid", + "Manually export keys": "Ekspordi võtmed käsitsi", + "You'll lose access to your encrypted messages": "Sa kaotad ligipääsu oma krüptitud sõnumitele", + "Are you sure you want to sign out?": "Kas sa oled kindel, et soovid välja logida?", + "Upload %(count)s other files|one": "Lae üles %(count)s muu fail", + "Cancel All": "Tühista kõik", + "Upload Error": "Üleslaadimise viga", + "Verify other session": "Verifitseeri teine sessioon", + "Verification Request": "Verifitseerimispäring", + "A widget would like to verify your identity": "Vidin soovib verifitseerida sinu isikut", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Vidin %(widgetUrl)s saidist soovib verifitseerida sinu isikut. Kui sa seda lubad, siis vidin verifitseerib vaid sinu kasutajatunnuse, kuid ei saa teha toiminguid sinuna.", + "Remember my selection for this widget": "Jäta meelde minu valik selle vidina kohta", + "Allow": "Luba", + "Deny": "Keela", + "Unable to restore backup": "Varukoopiast taastamine ei õnenstu", + "No backup found!": "Varukoopiat ei leidunud!", + "Keys restored": "Krüptimise võtmed on taastatud", + "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s sessiooni dekrüptimine ei õnnestunud!", + "Successfully restored %(sessionCount)s keys": "%(sessionCount)s sessiooni võtme taastamine õnnestus", + "Warning: you should only set up key backup from a trusted computer.": "Hoiatus: sa peaksid võtmete varunduse seadistama vaid usaldusväärsest arvutist." } diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 6548ea33d1..6624fa99bb 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -783,7 +783,7 @@ "Explore Account Data": "Miatu kontuaren datuak", "All messages (noisy)": "Mezu guztiak (ozen)", "Saturday": "Larunbata", - "I understand the risks and wish to continue": "Arriskua ulertzen dut eta jarraitu nahi dut", + "I understand the risks and wish to continue": "Arriskuak ulertzen ditut eta jarraitu nahi dut", "Direct Chat": "Txat zuzena", "The server may be unavailable or overloaded": "Zerbitzaria eskuraezin edo gainezka egon daiteke", "Reject": "Baztertu", @@ -2385,5 +2385,45 @@ "Please enter your recovery passphrase a second time to confirm.": "Sartu zure berreskuratze pasa-esaldia berriro baieztatzeko.", "Repeat your recovery passphrase...": "Errepikatu zure berreskuratze pasa-esaldia...", "Secure your backup with a recovery passphrase": "Babestu zure babeskopia berreskuratze pasa-esaldi batekin", - "Currently indexing: %(currentRoom)s": "Orain indexatzen: %(currentRoom)s" + "Currently indexing: %(currentRoom)s": "Orain indexatzen: %(currentRoom)s", + "Review where you’re logged in": "Berrikusi non hasi duzun saioa", + "New login. Was this you?": "Saio berria. Zu izan zara?", + "Opens chat with the given user": "Erabiltzailearekin txata irekitzen du", + "Sends a message to the given user": "Erabiltzaileari mezua bidaltzen dio", + "You signed in to a new session without verifying it:": "Saio berria hasi duzu hau egiaztatu gabe:", + "Verify your other session using one of the options below.": "Egiaztatu zure beste saioa beheko aukeretako batekin.", + "Font scaling": "Letren eskalatzea", + "Use the improved room list (in development - refresh to apply changes)": "Erabili gelen zerrenda hobetua (garapenean, freskatu aldaketak aplikatzedko)", + "Use IRC layout": "Erabili IRC diseinua", + "Font size": "Letra-tamaina", + "Custom font size": "Letra-tamaina pertsonalizatua", + "IRC display name width": "IRC-ko pantaila izenaren zabalera", + "Waiting for your other session to verify…": "Zure beste saioak egiaztatu bitartean zain…", + "Verify all your sessions to ensure your account & messages are safe": "Egiaztatu zure saio guztiak kontua eta mezuak seguru daudela bermatzeko", + "Verify the new login accessing your account: %(name)s": "Egiaztatu zure kontuan hasitako saio berria: %(name)s", + "Size must be a number": "Tamaina zenbaki bat izan behar da", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Letra tamaina pertsonalizatua %(min)s pt eta %(max)s pt bitartean egon behar du", + "Use between %(min)s pt and %(max)s pt": "Erabili %(min)s pt eta %(max)s pt bitarteko balioa", + "Appearance": "Itxura", + "Where you’re logged in": "Non hasi duzun saioa", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Kudeatu azpiko saioen izenak eta hauek amaitu edo egiaztatu zure erabiltzaile-profilean.", + "Create room": "Sortu gela", + "You've successfully verified your device!": "Ongi egiaztatu duzu zure gailua!", + "Message deleted": "Mezu ezabatuta", + "Message deleted by %(name)s": "Mezua ezabatu du %(name)s erabiltzaileak", + "QR Code": "QR kodea", + "To continue, use Single Sign On to prove your identity.": "Jarraitzeko, erabili Single Sign On zure identitatea frogatzeko.", + "Confirm to continue": "Berretsi jarraitzeko", + "Click the button below to confirm your identity.": "Sakatu azpiko botoia zure identitatea frogatzeko.", + "Invite someone using their name, username (like ), email address or share this room.": "Gonbidatu norbait bere izena, erabiltzaile izena (esaterako ), e-mail helbidea erabiliz, edo partekatu gela hau.", + "Restoring keys from backup": "Gakoak babes-kopiatik berrezartzen", + "Fetching keys from server...": "Gakoak zerbitzaritik eskuratzen...", + "%(completed)s of %(total)s keys restored": "%(completed)s/%(total)s gako berreskuratuta", + "Keys restored": "Gakoak berreskuratuta", + "Successfully restored %(sessionCount)s keys": "%(sessionCount)s gako ongi berreskuratuta", + "Confirm encryption setup": "Berretsi zifratze ezarpena", + "Click the button below to confirm setting up encryption.": "Sakatu azpiko botoia zifratze-ezarpena berresteko.", + "Dismiss read marker and jump to bottom": "Baztertu irakurtze-marka eta jauzi beheraino", + "Jump to oldest unread message": "Jauzi irakurri gabeko mezu zaharrenera", + "Upload a file": "Igo fitxategia" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index ae08572eb2..a3158f0280 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -421,7 +421,7 @@ "Invite new community members": "Kutsu uusia jäseniä yhteisöön", "Invite to Community": "Kutsu yhteisöön", "Which rooms would you like to add to this community?": "Mitkä huoneet haluaisit lisätä tähän yhteisöön?", - "Show these rooms to non-members on the community page and room list?": "Näytetäänkö nämä huoneet ei-jäsenille yhteisön sivulla ja huonelistassa?", + "Show these rooms to non-members on the community page and room list?": "Näytetäänkö nämä huoneet ei-jäsenille yhteisön sivulla ja huoneluettelossa?", "Add rooms to the community": "Lisää huoneita tähän yhteisöön", "Room name or alias": "Huoneen nimi tai alias", "Add to community": "Lisää yhteisöön", @@ -830,7 +830,7 @@ "Repeats like \"aaa\" are easy to guess": "Toistot, kuten ”aaa”, ovat helppoja arvata", "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Toistot, kuten ”abcabcabe” ovat vain hieman hankalampia arvata kuin ”abc”", "A word by itself is easy to guess": "Yksittäinen sana on helppo arvata", - "Please contact your homeserver administrator.": "Otathan yhteyttä kotipalvelimesi ylläpitäjään.", + "Please contact your homeserver administrator.": "Ota yhteyttä kotipalvelimesi ylläpitäjään.", "Show join/leave messages (invites/kicks/bans unaffected)": "Näytä liittymisten ja poistumisten viestit (ei vaikuta kutsuihin, huoneesta poistamisiin ja porttikieltoihin)", "Show developer tools": "Näytä kehitystyökalut", "Encrypted messages in one-to-one chats": "Salatut viestit kahdenkeskisissä keskusteluissa", @@ -1343,7 +1343,7 @@ "Deny": "Kiellä", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Tunnistimme dataa, joka on lähtöisin vanhasta Riotin versiosta. Tämä aiheuttaa toimintahäiriöitä osapuolten välisessä salauksessa vanhassa versiossa. Viestejä, jotka on salattu osapuolten välisellä salauksella vanhalla versiolla, ei välttämättä voida purkaa tällä versiolla. Tämä voi myös aiheuttaa epäonnistumisia viestien välityksessä tämän version kanssa. Jos kohtaat ongelmia, kirjaudu ulos ja takaisin sisään. Säilyttääksesi viestihistoriasi, vie salausavaimesi ja tuo ne uudelleen.", "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot epäonnistui protokollalistan hakemisessa kotipalvelimelta. Kotipalvelin saattaa olla liian vanha tukeakseen kolmannen osapuolen verkkoja.", - "Riot failed to get the public room list.": "Riot epäonnistui julkisen huonelistan haussa.", + "Riot failed to get the public room list.": "Riot ei onnistunut hakemaan julkista huoneluetteloa.", "The homeserver may be unavailable or overloaded.": "Kotipalvelin saattaa olla saavuttamattomissa tai ylikuormitettuna.", "You have %(count)s unread notifications in a prior version of this room.|other": "Sinulla on %(count)s lukematonta ilmoitusta huoneen edellisessä versiossa.", "You have %(count)s unread notifications in a prior version of this room.|one": "Sinulla on %(count)s lukematon ilmoitus huoneen edellisessä versiossa.", @@ -1374,7 +1374,7 @@ "Set up Secure Messages": "Ota käyttöön salatut viestit", "Recovery Method Removed": "Palautustapa poistettu", "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi.", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Käytätkö 'leivänmuruja' (kuvia huonelistan yläpuolella) vai et", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Käytätkö 'leivänmuruja' (kuvia huoneluettelon yläpuolella) vai et", "Replying With Files": "Tiedostoilla vastaaminen", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Tiedostolla vastaaminen ei onnistu tällä erää. Haluatko ladata tiedoston vastaamatta?", "The file '%(fileName)s' failed to upload.": "Tiedoston '%(fileName)s' lataaminen ei onnistunut.", @@ -2033,7 +2033,7 @@ "Support adding custom themes": "Tue mukaututettujen teemojen lisäämistä", "Enable cross-signing to verify per-user instead of per-session (in development)": "Ota ristivarmennus käyttöön varmentaaksesi käyttäjät istuntojen sijaan (kehitysversio)", "Show rooms with unread notifications first": "Näytä ensin huoneet, joissa on lukemattomia viestejä", - "Show shortcuts to recently viewed rooms above the room list": "Näytä oikotiet viimeiseksi katsottuihin huoneisiin huonelistan yläpuolella", + "Show shortcuts to recently viewed rooms above the room list": "Näytä oikotiet viimeiseksi katsottuihin huoneisiin huoneluettelon yläpuolella", "Enable message search in encrypted rooms": "Ota viestihaku salausta käyttävissä huoneissa käyttöön", "Keep secret storage passphrase in memory for this session": "Pidä salavaraston salalause muistissa tämän istunnon ajan", "How fast should messages be downloaded.": "Kuinka nopeasti viestit pitäisi ladata.", @@ -2061,7 +2061,7 @@ "Theme added!": "Teema lisätty!", "Add theme": "Lisää teema", "Scroll to most recent messages": "Vieritä tuoreimpiin viesteihin", - "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Huoneen vaihtoehtoisten osoitteiden päivittämisessä tapahtui virhe. Palvelin ei ehkä salli sitä tai syynä oli tilapäinen virhe.", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Huoneen vaihtoehtoisten osoitteiden päivittämisessä tapahtui virhe. Palvelin ei ehkä salli sitä tai kyseessä oli tilapäinen virhe.", "You don't have permission to delete the alias.": "Sinulla ei ole lupaa poistaa aliasta.", "Local address": "Paikallinen osoite", "Local Addresses": "Paikalliset osoitteet", @@ -2189,5 +2189,156 @@ "Successfully restored %(sessionCount)s keys": "%(sessionCount)s avaimen palautus onnistui", "This requires the latest Riot on your other devices:": "Tämä vaatii uusimman Riotin muilla laitteillasi:", "Currently indexing: %(currentRoom)s": "Indeksoidaan huonetta: %(currentRoom)s", - "Jump to oldest unread message": "Siirry vanhimpaan lukemattomaan viestiin" + "Jump to oldest unread message": "Siirry vanhimpaan lukemattomaan viestiin", + "Opens chat with the given user": "Avaa keskustelun annetun käyttäjän kanssa", + "Sends a message to the given user": "Lähettää viestin annetulle käyttäjälle", + "Manually Verify by Text": "Varmenna käsin tekstillä", + "Interactively verify by Emoji": "Varmenna interaktiivisesti emojilla", + "Use IRC layout": "Käytä IRC-asettelua", + "Enable cross-signing to verify per-user instead of per-session": "Ota ristivarmennus käyttöön varmentaaksesi käyttäjät istuntojen sijaan", + "Keep recovery passphrase in memory for this session": "Pidä palautuksen salalause muistissa tämän istunnon ajan", + "Manually verify all remote sessions": "Varmenna kaikki etäistunnot käsin", + "IRC display name width": "IRC-näyttönimen leveys", + "Verify this session by confirming the following number appears on its screen.": "Varmenna tämä istunto varmistamalla, että seuraava numero ilmestyy sen näytölle.", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Odotetaan toista istuntoasi, %(deviceName)s (%(deviceId)s), varmennukseen…", + "Waiting for your other session to verify…": "odotetaan toista istuntoasi varmennukseen…", + "Verify all your sessions to ensure your account & messages are safe": "Varmenna kaikki istuntosi varmistaaksesi, että tunnuksesi ja viestisi ovat turvassa", + "Verify the new login accessing your account: %(name)s": "Varmenna uusi tunnuksellesi sisäänkirjautunut taho: %(name)s", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Tällä hetkellä salasanan vaihtaminen nollaa kaikki osapuolten välisen salauksen avaimet kaikissa istunnoissa, tehden salatusta keskusteluhistoriasta lukukelvotonta, ellet ensin vie kaikkia huoneavaimiasi ja tuo niitä salasanan vaihtamisen jäkeen takaisin. Tulevaisuudessa tämä tulee toimimaan paremmin.", + "Your homeserver does not support cross-signing.": "Kotipalvelimesi ei tue ristivarmennusta.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Tunnuksellasi on ristivarmennuksen identiteetti salavarastossa, mutta tämä istunto ei luota siihen.", + "Reset cross-signing and secret storage": "Nollaa ristivarmennus ja salavarasto", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Varmenna jokainen käyttäjän istunto erikseen, äläkä luota ristivarmennettuihin laitteisiin.", + "Securely cache encrypted messages locally for them to appear in search results.": "Pidä salatut viestit turvallisessa välimuistissa, jotta ne näkyvät hakutuloksissa.", + "Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with search components added.": "Riotissa ei ole joitain komponentteja, joita tarvitaan viestien turvalliseen välimuistitallennukseen. Jos haluat kokeilla tätä ominaisuutta, käännä mukautettu Riot Desktop, jossa on mukana hakukomponentit.", + "Riot can't securely cache encrypted messages locally while running in a web browser. Use Riot Desktop for encrypted messages to appear in search results.": "Riot ei voi tallentaa viestejä turvalliseen välimuistiin pyöriessään selaimessa. Käytä Electron-pohjaista Riot Desktop-sovellusta nähdäksesi salatut viestit hakutuloksissa.", + "This session is backing up your keys. ": "Tämä istunto varmuuskopioi avaimesi. ", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Tämä istunto ei varmuuskopioi avaimiasi, mutta sillä on olemassaoleva varmuuskopio, jonka voit palauttaa ja lisätä jatkaaksesi.", + "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Yhdistä tämä istunto avainten varmuuskopiointiin ennen uloskirjautumista, jotta et menetä avaimia, jotka ovat vain tässä istunnossa.", + "Connect this session to Key Backup": "Yhdistä tämä istunto avainten varmuuskopiointiin", + "Backup has a valid signature from verified session ": "Varmuuskopiossa on kelvollinen allekirjoitus varmennetusta istunnosta ", + "Backup has a valid signature from unverified session ": "Varmuuskopiossa on kelvollinen allekirjoitus varmentamattomasta istunnosta ", + "Backup has an invalid signature from verified session ": "Varmuuskopiossa on epäkelpo allekirjoitus varmennetusta istunnosta ", + "Backup has an invalid signature from unverified session ": "Varmuuskopiossa on epäkelpo allekirjoitus varmentamattomasta istunnosta ", + "This backup is trusted because it has been restored on this session": "Tähän varmuuskopioon luotetaan, koska se on palautettu tässä istunnossa", + "Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Vara-avain on salavarastossa, mutta salavarasto ei ole käytössä tässä istunnossa. Ota ristivarmennus käyttöön Laboratoriosta muokkaaksesi avainten varmuuskopioinnin tilaa.", + "Your keys are not being backed up from this session.": "Avaimiasi ei varmuuskopioida tästä istunnosta.", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Salasanasi on onnistuneesti vaihdettu. Et saa ilmoituksia muilla laitteillasi ennen kuin kirjaudut niillä takaisin sisään", + "Invalid theme schema.": "Epäkelpo teeman skeema.", + "Custom theme URL": "Mukautettu teeman osoite", + "Keyboard Shortcuts": "Pikanäppäimet", + "Session ID:": "Istunnon tunnus:", + "Session key:": "Istunnon avain:", + "Where you’re logged in": "Missä olet sisäänkirjautuneena", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Muokkaa istuntojesi nimiä ja kirjaudu niistä ulos alapuolella tai varmenna ne käyttäjäprofiilissasi.", + "This user has not verified all of their sessions.": "Tämä käyttäjä ei ole varmentanut kaikkia istuntojaan.", + "You have not verified this user.": "Et ole varmentanut tätä käyttäjää.", + "You have verified this user. This user has verified all of their sessions.": "Olet varmentanut tämän käyttäjän. Tämä käyttäjä on varmentanut kaikki istuntonsa.", + "This room is end-to-end encrypted": "Tämä huone käyttää osapuolten välistä salausta", + "Everyone in this room is verified": "Kaikki tämän huoneen käyttäjät on varmennettu", + "Some sessions for this user are not trusted": "Osaan tämän käyttäjän istunnoista ei luoteta", + "All sessions for this user are trusted": "Kaikkiin tämän käyttäjän istunnoista luotetaan", + "Some sessions in this encrypted room are not trusted": "Osaan tämän salausta käyttävän huoneen istunnoista ei luoteta", + "All sessions in this encrypted room are trusted": "Kaikkiin tämän salausta käyttävän huoneen istuntoihin luotetaan", + "Your key share request has been sent - please check your other sessions for key share requests.": "Avainten jakopyyntösi on lähetetty. Tarkista muut istuntosi avainten jakopyyntöjen varalta.", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Avainten jakopyynnöt lähetetään muille istunnoillesi automaattisesti. Jos hylkäsit tai jätit huomiotta avainten jakopyynnön toisessa istunnossasi, klikkaa tästä pyytääksesi avaimia uudelleen.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Jos muissa laitteissasi ei ole avainta tämän viestin purkamiseen, niillä istunnoilla ei voi lukea tätä viestiä.", + "Encrypted by an unverified session": "Salattu varmentamattoman istunnon toimesta", + "Encrypted by a deleted session": "Salattu poistetun istunnon toimesta", + "No sessions with registered encryption keys": "Yhdelläkään istunnolla ei ole rekisteröityjä salausavaimia", + "Create room": "Luo huone", + "Reject & Ignore user": "Hylkää ja jätä käyttäjä huomiotta", + "Start Verification": "Aloita varmennus", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Viestisi ovat turvattu, ja vain sinulla ja vastaanottajalla on avaimet viestien lukemiseen.", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Salausta käyttävissä huoneissa viestisi on turvattu, ja vain sinulla ja vastaanottajilla on yksityiset avaimet viestien lukemiseen.", + "Verify User": "Varmenna käyttäjä", + "For extra security, verify this user by checking a one-time code on both of your devices.": "Lisäturvaksi, varmenna tämä käyttäjä tarkistamalla koodin kummankin laitteella.", + "The homeserver the user you’re verifying is connected to": "Käyttäjä, jota varmennat, on kotipalvelimella", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "Istunto, jota yrität varmentaa, ei tue QR-koodin skannausta tai emoji-varmennusta, joita Riot tukee. Kokeile eri asiakasohjelmalla.", + "Verify by scanning": "Varmenna skannaamalla", + "If you can't scan the code above, verify by comparing unique emoji.": "Jos et pysty skannaamaan yläpuolella olevaa koodia, varmenna vertaamalla emojia.", + "Verify by comparing unique emoji.": "Varmenna vertaamalla uniikkia emojia.", + "Verify by emoji": "Varmenna emojilla", + "Verify all users in a room to ensure it's secure.": "Varmenna kaikki huoneen käyttäjät varmistaaksesi, että se on turvallinen.", + "In encrypted rooms, verify all users to ensure it’s secure.": "Varmenna kaikki käyttäjät salausta käyttävissä huoneissa, jotta huone on varmasti turvallinen.", + "You've successfully verified your device!": "Olet onnistuneesti varmentanut laitteesi!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Olet onnistuneesti varmentanut laitteen %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "Olet varmentanut käyttäjän %(displayName)s!", + "Verified": "Varmennettu", + "Font size": "Fonttikoko", + "Custom font size": "Mukautettu fonttikoko", + "Size must be a number": "Koon täytyy olla luku", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Mukautetun fonttikoon täytyy olla vähintään %(min)s pt ja enintään %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Käytä kokoa väliltä %(min)s pt ja %(max)s pt", + "Appearance": "Ulkoasu", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Et voi poistaa tätä käytöstä jatkossa. Sillat ja useimmat botit eivät vielä toimi.", + "Navigate up/down in the room list": "Siirry huoneluettelossa ylöspäin/alaspäin", + "Select room from the room list": "Valitse huone huoneluettelosta", + "Previous/next unread room or DM": "Edellinen/seuraava lukematon huone tai yksityisviesti", + "Previous/next room or DM": "Edellinen/seuraava huone tai yksityisviesti", + "Toggle this dialog": "Tämä valintaikkuna päälle/pois", + "Use the improved room list (in development - refresh to apply changes)": "Käytä parannettua huoneluetteloa (kehitysversio — päivitä sivu ottaaksesi muutokset käyttöön)", + "Start verification again from the notification.": "Aloita varmennus uudelleen ilmoituksesta.", + "Start verification again from their profile.": "Aloita varmennus uudelleen hänen profiilista.", + "Verification timed out.": "Varmennuksessa kesti liikaa.", + "You cancelled verification on your other session.": "Peruutit varmennuksen toisessa istunnossasi.", + "%(displayName)s cancelled verification.": "%(displayName)s peruutti varmennuksen.", + "You cancelled verification.": "Peruutit varmennuksen.", + "Verification cancelled": "Varmennus peruutettu", + "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Tämän huoneen viestit ovat salattuja osapuolten välisellä salauksella. Lue lisää ja varmenna tämä käyttäjä hänen profiilistaan.", + "Enter the name of a new server you want to explore.": "Syötä sen uuden palvelimen nimi, jota hauat tutkia.", + "%(networkName)s rooms": "Verkon %(networkName)s huoneet", + "Destroy cross-signing keys?": "Tuhoa ristivarmennuksen avaimet?", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Ristivarmennuksen avainten tuhoamista ei voi kumota. Jokainen, jonka olet varmentanut, tulee näkemään turvallisuushälytyksiä. Et todennäköisesti halua tehdä tätä, ellet ole hukannut kaikkia laitteitasi, joista pystyt ristivarmentamaan.", + "Clear cross-signing keys": "Tyhjennä ristivarmennuksen avaimet", + "Enable end-to-end encryption": "Ota osapuolten välinen salaus käyttöön", + "To verify that this session can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Jotta tähän istuntoon voitaisiin luottaa, tarkista, että käyttäjän asetuksissa näkyvä avain täsmää alapuolella olevaan avaimeen:", + "To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:": "Jotta tähän istuntoon voitaisiin luottaa, ota yhteyttä sen omistajaan jotain muuta kautta (esim. kasvotusten tai puhelimitse) ja kysy, että täsmääkö hänen käyttäjäasetuksissa näkemänsä istunnon avain alla olevaan:", + "Session key": "Istunnon tunnus", + "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this session and you probably want to press the blacklist button instead.": "Jos se täsmää, paina varmennuspainiketta alapuolella. Jos se ei täsmää, joku häiritsee tätä istuntoa ja haluat luultavasti painaa estä -painiketta sen sijaan.", + "Verification Requests": "Varmennuspyynnöt", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Tämän käyttäjän varmentaminen merkitsee hänen istuntonsa luotetuksi, ja myös merkkaa sinun istuntosi luotetuksi hänen laitteissaan.", + "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.": "Varmenna tämä laite merkataksesi se luotetuksi. Tähän laitteeseen luottaminen antaa sinulle ja muille käyttäjille ylimääräistä mielenrauhaa, kun käytätte osapuolten välistä salausta.", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Tämän laitteen varmentaminen merkkaa sen luotetuksi, ja sinut varmentaneet käyttäjät luottavat automaattisesti tähän laitteeseen.", + "Confirm to continue": "Haluan jatkaa", + "Click the button below to confirm your identity.": "Paina alapuolella olevaa painiketta varmistaaksesi identiteettisi.", + "We couldn't create your DM. Please check the users you want to invite and try again.": "Emme onnistuneet luomaan yksityisviestiä. Tarkista, että kutsumasi henkilöt haluavat kutsusi ja yritä uudelleen.", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Seuraavat käyttäjät eivät välttämättä ole olemassa tai ne ovat epäkelpoja, joten niitä ei voida kutsua: %(csvNames)s", + "Recently Direct Messaged": "Viimeaikaiset yksityisviestit", + "Start a conversation with someone using their name, username (like ) or email address.": "Aloita keskustelu jonkun kanssa käyttäen hänen nimeä, käyttäjätunnus (kuten ) tai sähköpostiosoitetta.", + "Invite someone using their name, username (like ), email address or share this room.": "Kutsu tähän huoneeseen käyttäen nimeä, käyttäjätunnusta (kuten ), sähköpostiosoitetta tai jaa tämä huone.", + "Your unverified session '%(displayName)s' is requesting encryption keys.": "Varmentamaton istuntosi '%(displayName)s' pyytää salausavaimia.", + "Riot encountered an error during upload of:": "Riot kohtasi virheen lähettäessään:", + "Upload completed": "Lähetys valmis", + "Cancelled signature upload": "Allekirjoituksen lähetys peruutettu", + "Unable to upload": "Lähettäminen ei ole mahdollista", + "Signature upload success": "Allekirjoituksen lähettäminen onnistui", + "Signature upload failed": "Allekirjoituksen lähettäminen epäonnistui", + "Room name or address": "Huoneen nimi tai osoite", + "Joins room with given address": "Liittyy annetun osoitteen mukaiseen huoneeseen", + "Unrecognised room address:": "Tunnistamaton huoneen osoite:", + "Help us improve Riot": "Auta parantamaan Riotia", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Lähetä anonyymiä käyttötietoa, joka auttaa Riotin kehittämisessä. Toiminto käyttää evästettä.", + "I want to help": "Haluan auttaa", + "Your homeserver has exceeded its user limit.": "Kotipalvelimesi on ylittänyt käyttäjärajansa.", + "Your homeserver has exceeded one of its resource limits.": "Kotipalvelimesi on ylittänyt jonkin resurssirajansa.", + "Contact your server admin.": "Ota yhteyttä palvelimesi ylläpitäjään.", + "Ok": "OK", + "Set password": "Aseta salasana", + "To return to your account in future you need to set a password": "Päästäksesi jatkossa takaisin tilillesi sinun täytyy asettaa salasana", + "Restart": "Käynnistä uudelleen", + "Upgrade your Riot": "Päivitä Riot", + "A new version of Riot is available!": "Uusi Riotin versio on saatavilla!", + "New version available. Update now.": "Uusi versio saatavilla. Päivitä nyt.", + "To link to this room, please add an address.": "Lisää osoite linkittääksesi tähän huoneeseen.", + "Error creating address": "Virhe osoitetta luotaessa", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Osoitetta luotaessa tapahtui virhe. Voi olla, että palvelin ei salli sitä tai kyseessä oli tilapäinen virhe.", + "You don't have permission to delete the address.": "Sinulla ei ole oikeutta poistaa osoitetta.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Osoitetta poistaessa tapahtui virhe. Osoitetta ei ehkä ole enää olemassa tai kyseessä oli tilapäinen virhe.", + "Error removing address": "Virhe osoitetta poistettaessa", + "Categories": "Luokat", + "This address is available to use": "Tämä osoite on käytettävissä", + "This address is already in use": "Tämä osoite on jo käytössä", + "Set a room address to easily share your room with other people.": "Aseta huoneelle osoite, jotta voit jakaa huoneen helposti muille.", + "Address (optional)": "Osoite (valinnainen)" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c231769f27..1bd6fd254a 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2435,5 +2435,51 @@ "QR Code": "Code QR", "Dismiss read marker and jump to bottom": "Ignorer le signet de lecture et aller en bas", "Jump to oldest unread message": "Aller au plus vieux message non lu", - "Upload a file": "Envoyer un fichier" + "Upload a file": "Envoyer un fichier", + "Use IRC layout": "Utiliser la mise en page d’IRC", + "IRC display name width": "Largeur du nom affiché IRC", + "Create room": "Créer un salon", + "Font scaling": "Mise à l’échelle de la police", + "Font size": "Taille de la police", + "Custom font size": "Taille personnalisée de la police", + "Size must be a number": "La taille doit être un nombre", + "Custom font size can only be between %(min)s pt and %(max)s pt": "La taille de police personnalisée doit être comprise entre %(min)s pt et %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Utiliser entre %(min)s pt et %(max)s pt", + "Appearance": "Apparence", + "Use the improved room list (in development - refresh to apply changes)": "Utiliser la liste de salons améliorée (en développement − actualisez pour appliquer les changements)", + "Room name or address": "Nom ou adresse du salon", + "Joins room with given address": "Rejoint le salon à l’adresse donnée", + "Unrecognised room address:": "Adresse de salon non reconnue :", + "Please verify the room ID or address and try again.": "Vérifiez l’identifiant ou l’adresse du salon et réessayez.", + "Room ID or address of ban list": "Identifiant du salon ou adresse de la liste de bannissement", + "To link to this room, please add an address.": "Pour créer un lien vers ce salon, ajoutez une adresse.", + "Error creating address": "Erreur lors de la création de l’adresse", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Une erreur est survenue lors de la création de l’adresse. Ce n’est peut-être pas autorisé par le serveur ou une erreur temporaire est survenue.", + "You don't have permission to delete the address.": "Vous n’avez pas la permission de supprimer cette adresse.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Une erreur est survenue lors de la suppression de cette adresse. Elle n’existe peut-être plus ou une erreur temporaire est survenue.", + "Error removing address": "Erreur lors de la suppression de l’adresse", + "Categories": "Catégories", + "Room address": "Adresse du salon", + "Please provide a room address": "Veuillez fournir une adresse de salon", + "This address is available to use": "Cette adresse est disponible", + "This address is already in use": "Cette adresse est déjà utilisée", + "Set a room address to easily share your room with other people.": "Définissez une adresse de salon pour le partager facilement avec d’autres personnes.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Vous avez précédemment utilisé une version plus récente de Riot avec cette session. Pour réutiliser cette version avec le chiffrement de bout en bout, vous devrez vous déconnecter et vous reconnecter.", + "Address (optional)": "Adresse (optionnel)", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Supprimer l’adresse du salon %(alias)s et supprimer %(name)s du répertoire ?", + "delete the address.": "supprimer l’adresse.", + "Use a different passphrase?": "Utiliser une phrase secrète différente ?", + "Help us improve Riot": "Aidez-nous à améliorer Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Envoyez des données anonymes d’utilisation qui nous aident à améliorer Riot. Cela utilisera un cookie.", + "I want to help": "Je veux aider", + "Your homeserver has exceeded its user limit.": "Votre serveur d’accueil a dépassé ses limites d’utilisateurs.", + "Your homeserver has exceeded one of its resource limits.": "Votre serveur d’accueil a dépassé une de ses limites de ressources.", + "Contact your server admin.": "Contactez l’administrateur de votre serveur.", + "Ok": "OK", + "Set password": "Définir le mot de passe", + "To return to your account in future you need to set a password": "Pour réutiliser votre compte à l’avenir, vous devez définir un mot de passe", + "Restart": "Redémarrer", + "Upgrade your Riot": "Mettre à niveau votre Riot", + "A new version of Riot is available!": "Une nouvelle version de Riot est disponible !", + "New version available. Update now.": "Nouvelle version disponible. Faire la mise à niveau maintenant." } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 3328292be0..d37abfb890 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -138,7 +138,7 @@ "Your browser does not support the required cryptography extensions": "O seu navegador non soporta as extensións de criptografía necesarias", "Not a valid Riot keyfile": "Non é un ficheiro de chaves Riot válido", "Authentication check failed: incorrect password?": "Fallou a comprobación de autenticación: contrasinal incorrecto?", - "Failed to join room": "Non se puido unir a sala", + "Failed to join room": "Non puideches entrar na sala", "Message Pinning": "Fixando mensaxe", "Use compact timeline layout": "Utilizar a disposición compacta da liña temporal", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostrar marcas de tempo con formato 12 horas (ex. 2:30pm)", @@ -189,7 +189,7 @@ "Drop File Here": "Solte aquí o ficheiro", "Drop file here to upload": "Solte aquí o ficheiro para subilo", " (unsupported)": " (non soportado)", - "Join as voice or video.": "Únase como voz ou vídeo.", + "Join as voice or video.": "Únete como voz ou vídeo.", "Ongoing conference call%(supportedText)s.": "Chamada de conferencia en curso%(supportedText)s.", "%(senderName)s sent an image": "%(senderName)s enviou unha imaxe", "%(senderName)s sent a video": "%(senderName)s enviou un vídeo", @@ -264,8 +264,8 @@ "Seen by %(userName)s at %(dateTime)s": "Visto por %(userName)s as %(dateTime)s", "No rooms to show": "Sen salas que mostrar", "Unnamed room": "Sala sen nome", - "World readable": "Visible por todos", - "Guests can join": "Convidados pódense unir", + "World readable": "Visible para todas", + "Guests can join": "Convidadas pódense unir", "Save": "Gardar", "(~%(count)s results)|other": "(~%(count)s resultados)", "(~%(count)s results)|one": "(~%(count)s resultado)", @@ -294,7 +294,7 @@ "This room is not accessible by remote Matrix servers": "Esta sala non é accesible por servidores Matrix remotos", "Leave room": "Deixar a sala", "Favourite": "Favorita", - "Guests cannot join this room even if explicitly invited.": "Os convidados non se poden unir a esta sala inda que fosen convidados explicitamente.", + "Guests cannot join this room even if explicitly invited.": "As convidadas non se poden unir a esta sala ainda que fosen convidadas explicitamente.", "Click here to fix": "Pulse aquí para solución", "Who can access this room?": "Quen pode acceder a esta sala?", "Only people who have been invited": "Só persoas que foron convidadas", @@ -503,7 +503,7 @@ "Alias (optional)": "Alias (opcional)", "Name": "Nome", "You must register to use this functionality": "Debe rexistrarse para utilizar esta función", - "You must join the room to see its files": "Debe unirse a sala para ver os seus ficheiros", + "You must join the room to see its files": "Debes unirte a sala para ver os seus ficheiros", "There are no visible files in this room": "Non hai ficheiros visibles nesta sala", "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even use 'img' tags\n

    \n": "

    HTML para a páxina da súa comunidade

    \n

    \n Utilice a descrición longa para presentar novos membros a comunidade, ou publicar algunha ligazón importante\n \n

    \n

    \n Tamén pode utilizar etiquetas 'img'\n

    \n", "Add rooms to the community summary": "Engadir salas ao resumo da comunidade", @@ -527,11 +527,11 @@ "Leave %(groupName)s?": "Deixar %(groupName)s?", "Leave": "Saír", "Community Settings": "Axustes da comunidade", - "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Estas salas móstranselle aos membros da comunidade na páxina da comunidade.Os participantes da comunidade poden unirse ás salas premendo nelas.", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Estas salas móstranse aos membros da comunidade na páxina da comunidade. Os participantes da comunidade poden unirse ás salas premendo nelas.", "Add rooms to this community": "Engadir salas a esta comunidade", "Featured Rooms:": "Salas destacadas:", "Featured Users:": "Usuarios destacados:", - "%(inviter)s has invited you to join this community": "%(inviter)s convidouna a unirse a esta comunidade", + "%(inviter)s has invited you to join this community": "%(inviter)s convidoute a entrar nesta comunidade", "You are an administrator of this community": "Vostede administra esta comunidade", "You are a member of this community": "É membro desta comunidade", "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "A súa comunidade non ten unha descrición longa, ou unha páxina HTML que lle mostrar aos seus participantes.
    Pulse aquí para abrir os axustes e publicar unha!", @@ -545,13 +545,13 @@ "Are you sure you want to leave the room '%(roomName)s'?": "Seguro que desexa saír da sala '%(roomName)s'?", "Failed to leave room": "Algo fallou ao saír da sala", "Signed Out": "Desconectada", - "For security, this session has been signed out. Please sign in again.": "Por seguridade, pechouse a sesión. Por favor, conéctese de novo.", + "For security, this session has been signed out. Please sign in again.": "Por seguridade, pechouse a sesión. Por favor, conéctate outra vez.", "Old cryptography data detected": "Detectouse o uso de criptografía sobre datos antigos", "Logout": "Desconectar", "Your Communities": "As súas Comunidades", "Error whilst fetching joined communities": "Fallo mentres se obtiñas as comunidades unidas", "Create a new community": "Crear unha nova comunidade", - "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crear unha comunidade para agrupar usuarias e salas! Poña unha páxina de inicio personalizada para destacar o seu lugar no universo Matrix.", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crea unha comunidade para agrupar usuarias e salas! Pon unha páxina de inicio personalizada para destacar o teu lugar no universo Matrix.", "You have no visible notifications": "Non ten notificacións visibles", "Scroll to bottom of page": "Desprácese ate o final da páxina", "%(count)s of your messages have not been sent.|other": "Algunha das súas mensaxes non foron enviadas.", @@ -621,10 +621,10 @@ "A new password must be entered.": "Debe introducir un novo contrasinal.", "New passwords must match each other.": "Os novos contrasinais deben ser coincidentes.", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Detectáronse datos de una versión anterior de Riot. Isto causará un mal funcionamento da criptografía extremo-a-extremo na versión antiga. As mensaxes cifradas extremo-a-extremo intercambiadas mentres utilizaba a versión anterior poderían non ser descifrables en esta versión. Isto tamén podería causar que mensaxes intercambiadas con esta versión tampouco funcionasen. Se ten problemas, desconéctese e conéctese de novo. Para manter o historial de mensaxes, exporte e reimporte as súas chaves.", - "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Enviouse un correo a %(emailAddress)s. Unha vez siga a ligazón que contén, pulse abaixo.", - "I have verified my email address": "Validei o meu enderezo de correo electrónico", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Enviouse un correo a %(emailAddress)s. Unha vez sigas a ligazón que contén, preme embaixo.", + "I have verified my email address": "Validei o meu enderezo de email", "Return to login screen": "Volver a pantalla de conexión", - "Send Reset Email": "Enviar correo electrónico de restablecemento", + "Send Reset Email": "Enviar email de restablecemento", "Incorrect username and/or password.": "Nome de usuaria ou contrasinal non válidos.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Teña en conta que se está a conectar ao servidor %(hs)s, non a matrix.org.", "The phone number entered looks invalid": "O número de teléfono introducido non semella ser válido", @@ -670,8 +670,8 @@ "Passphrases must match": "As frases de paso deben coincidir", "Passphrase must not be empty": "A frase de paso non pode quedar baldeira", "Export room keys": "Exportar chaves da sala", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Este proceso permítelle exportar a un ficheiro local as chaves para as mensaxes que recibiu en salas cifradas. Posteriormente permitiralle importar as chaves en outro cliente Matrix no futuro, así o cliente poderá descifrar esas mensaxes.", - "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.": "O ficheiro exportado permitiralle a calquera que poida lelo descifrar e cifrar mensaxes que vostede ve, así que debería ter coidado e gardalo de xeito seguro. Para axudarlle, debe introducir unha frase de paso aquí abaixo que será utilizada para cifrar os datos exportados. Só será posible importar os datos utilizando a mesma frase de paso.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Este proceso permíteche exportar a un ficheiro local as chaves para as mensaxes que recibiches en salas cifradas. Após poderás importar as chaves noutro cliente Matrix no futuro, así o cliente poderá descifrar esas mensaxes.", + "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.": "O ficheiro exportado permitiralle a calquera que poida lelo descifrar e cifrar mensaxes que ti ves, así que deberías ter coidado e gardalo de xeito seguro. Para axudarche, deberías escribir unha frase de paso aquí abaixo que será usada para cifrar os datos exportados. Só será posible importar os datos utilizando a mesma frase de paso.", "Enter passphrase": "Introduza a frase de paso", "Confirm passphrase": "Confirme a frase de paso", "Export": "Exportar", @@ -701,17 +701,17 @@ "Flair": "Popularidade", "Showing flair for these communities:": "Mostrar a popularidade destas comunidades:", "Display your community flair in rooms configured to show it.": "Mostrar a popularidade da túa comunidade nas salas configuradas para que a mostren.", - "Did you know: you can use communities to filter your Riot.im experience!": "Sabía que pode utilizar as comunidades para mellorar a súa experiencia con Riot.im!", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Para establecer un filtro, arrastre un avatar da comunidade sobre o panel de filtros na parte esquerda da pantalla. Pode pulsar nun avatar no panel de filtrado en calquera momento para ver só salas e xente asociada a esa comunidade.", + "Did you know: you can use communities to filter your Riot.im experience!": "Sabías que podes usar as comunidades para filtrar a túa experiencia en Riot.im!", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Para establecer un filtro, arrastra un avatar da comunidade sobre o panel de filtros na parte esquerda da pantalla. Podes premer nun avatar no panel de filtrado en calquera momento para ver só salas e xente asociada a esa comunidade.", "Deops user with given id": "Degradar o usuario con esa ID", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Visto por %(displayName)s(%(userName)s en %(dateTime)s", "Code": "Código", - "Unable to join community": "Non se puido unir a comunidade", + "Unable to join community": "Non te puideches unir a comunidade", "Unable to leave community": "Non se puido deixar a comunidade", "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Os cambios realizados a súa comunidade name e avatar poida que non os vexan outros usuarios ate dentro de 30 minutos.", - "Join this community": "Únase a esta comunidade", + "Join this community": "Únete a esta comunidade", "Leave this community": "Deixar esta comunidade", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si enviou un reporte de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso do aplicativo incluíndo o seu nome de usuaria, os IDs ou alcumes das salas e grupos que visitou e os nomes de usuaria de outras persoas. Non conteñen mensaxes.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se enviaches un informe de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso da aplicación incluíndo o teu nome de usuaria, os IDs ou alcumes das salas e grupos que visitaches e os nomes de usuaria de outras persoas. Non conteñen mensaxes.", "Submit debug logs": "Enviar informes de depuración", "Opens the Developer Tools dialog": "Abre o cadro de Ferramentas de desenvolvemento", "Stickerpack": "Iconas", @@ -756,7 +756,7 @@ "Please set a password!": "Por favor estableza un contrasinal!", "You have successfully set a password!": "Mudou con éxito o seu contrasinal!", "An error occurred whilst saving your email notification preferences.": "Algo fallou mentres se gardaban as súas preferencias de notificación.", - "Explore Room State": "Explorar estado da sala", + "Explore Room State": "Ollar estado da sala", "Source URL": "URL fonte", "Messages sent by bot": "Mensaxes enviadas por bot", "Filter results": "Filtrar resultados", @@ -779,7 +779,7 @@ "Developer Tools": "Ferramentas para desenvolver", "Preparing to send logs": "Preparándose para enviar informe", "Remember, you can always set an email address in user settings if you change your mind.": "Lembre que sempre poderá poñer un enderezo de correo nos axustes de usuario se cambiase de idea.", - "Explore Account Data": "Explorar datos da conta", + "Explore Account Data": "Ollar datos da conta", "All messages (noisy)": "Todas as mensaxes (alto)", "Saturday": "Sábado", "I understand the risks and wish to continue": "Entendo os riscos e desexo continuar", @@ -821,7 +821,7 @@ "Show message in desktop notification": "Mostrar mensaxe nas notificacións de escritorio", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os informes de depuración conteñen datos de utilización do aplicativo como o seu nome de usuario, os IDs ou alcumes de salas e grupos que vostede visitou e os nomes de usuarios doutras usuarias. Non conteñen mensaxes.", "Unhide Preview": "Desagochar a vista previa", - "Unable to join network": "Non se puido conectar a rede", + "Unable to join network": "Non se puido conectar ca rede", "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Pode que os configurases nun cliente diferente de Riot. Non podes establecelos desde Riot pero aínda así aplicaranse", "Sorry, your browser is not able to run Riot.": "Desculpe, o seu navegador non pode executar Riot.", "Uploaded on %(date)s by %(user)s": "Subido a %(date)s por %(user)s", @@ -834,7 +834,7 @@ "Off": "Off", "Riot does not know how to join a room on this network": "Riot non sabe como conectar cunha sala nesta rede", "Mentions only": "Só mencións", - "You can now return to your account after signing out, and sign in on other devices.": "Pode volver a súa contra tras desconectarse, e conectarse en outros dispositivos.", + "You can now return to your account after signing out, and sign in on other devices.": "Podes voltar a túa conta tras desconectarte, e conectarte noutros dispositivos.", "Enable email notifications": "Activar notificacións de correo", "Event Type": "Tipo de evento", "Download this file": "Descargue este ficheiro", @@ -860,17 +860,17 @@ "Clear Storage and Sign Out": "Limpar o almacenamento e Desconectar", "Refresh": "Actualizar", "We encountered an error trying to restore your previous session.": "Atopamos un fallo intentando restablecer a súa sesión anterior.", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Limpando o almacenamento do navegador podería resolver o problema, pero desconectarao e non poderá ler o historial cifrado da conversa.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Limpando o almacenamento do navegador podería resolver o problema, pero desconectarate e non poderás ler o historial cifrado da conversa.", "Collapse Reply Thread": "Comprimir o fío de respostas", "e.g. %(exampleValue)s": "p.ex. %(exampleValue)s", "Send analytics data": "Enviar datos de análises", "Enable widget screenshots on supported widgets": "Activar as capturas de trebellos para aqueles que as permiten", - "Share Link to User": "Compartir a ligazón co usuario", + "Share Link to User": "Compartir a ligazón coa usuaria", "Share room": "Compartir sala", "Muted Users": "Usuarios silenciados", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Mellore Riot.im enviando os datos anónimos de uso. Iso suporá o emprego dunha cookie (véxase a nosa Política de Cookies).", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Mellore Riot.im enviando o uso de datos anónimo. Iso usará unha cookie.", - "Yes, I want to help!": "Si, quero axuda", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Axuda a mellorar Riot.im enviando os datos anónimos de uso. Usaremos unha cookie (le aquí a nosa Política de Cookies).", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Axuda a mellorar Riot.im enviando datos anónimos de uso. Esto usará unha cookie.", + "Yes, I want to help!": "Si, quero axudar!", "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Iso fará que a súa deixe de ter uso de xeito permanente. Non poderá acceder e ninguén vai a poder volver a rexistrar esa mesma ID de usuario. Suporá que saía de todas as salas de conversas nas que estaba e eliminará os detalles da súa conta do servidores de identificación.Isto non se poderá desfacer", "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Desactivando a súa conta non supón que por defecto esquezamos as súas mensaxes enviadas. Se quere que nos esquezamos das súas mensaxes, prema na caixa de embaixo.", "To continue, please enter your password:": "Para continuar introduza o seu contrasinal:", @@ -936,6 +936,322 @@ "Whether you're using Riot on a device where touch is the primary input mechanism": "Se estás conectada utilizando Riot nun dispositivo maiormente táctil", "Whether you're using Riot as an installed Progressive Web App": "Se estás a usar Riot como unha Progressive Web App instalada", "Your user agent": "User Agent do navegador", - "The information being sent to us to help make Riot better includes:": "Información que nos envías para mellorar Riot inclúe:", - "Please install Chrome, Firefox, or Safari for the best experience.": "Instala Chrome, Firefox, ou Safari para ter unha mellor experiencia." + "The information being sent to us to help make Riot better includes:": "A información que nos envías para mellorar Riot inclúe:", + "Please install Chrome, Firefox, or Safari for the best experience.": "Instala Chrome, Firefox, ou Safari para ter unha mellor experiencia.", + "Sign In or Create Account": "Conéctate ou Crea unha Conta", + "Sign In": "Conectar", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirma o borrado destas sesións ao usar Single Sign On como proba da túa identidade.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirma o borrado desta sesión ao utilizar Single Sign On como proba da túa identidade.", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Xestiona os nomes e pecha as sesións embaixo ou verificaas no teu Perfil de Usuaria.", + "Sign Up": "Rexistro", + "Sign in with single sign-on": "Conectar usando Single Sign On", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "O eliminación das chaves de sinatura cruzada é permanente. Calquera a quen verificases con elas verá alertas de seguridade. Seguramente non queres facer esto, a menos que perdeses todos os dispositivos nos que podías asinar.", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Usaches anteriormente unha versión máis recente de Riot en %(host)s. Para usar esta versión de novo con cifrado E2E, tes que desconectar e conectar outra vez. ", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirma a desactivación da túa conta usando Single Sign On para probar a túa identidade.", + "To continue, use Single Sign On to prove your identity.": "Para continuar, usa Single Sign On para probar a túa identidade.", + "Are you sure you want to sign out?": "Tes a certeza de querer desconectar?", + "If you didn’t sign in to this session, your account may be compromised.": "Se ti non iniciaches esta sesión a túa conta podería estar comprometida.", + "Sign out and remove encryption keys?": "Desconectar e eliminar as chaves de cifrado?", + "This will allow you to return to your account after signing out, and sign in on other sessions.": "Esto permitirache voltar a túa conta tras desconectar, e conectarte noutras sesións.", + "Sign in to your Matrix account on %(serverName)s": "Conecta a túa conta Matrix en %(serverName)s", + "Sign in to your Matrix account on ": "Conecta a túa conta Matrix en ", + "Sign in with SSO": "Conecta utilizando SSO", + "Sign in instead": "Conectar", + "A verification email will be sent to your inbox to confirm setting your new password.": "Ímosche enviar un email para confirmar o teu novo contrasinal.", + "Your password has been reset.": "Restableceuse o contrasinal.", + "Enter your password to sign in and regain access to your account.": "Escribe o contrasinal para conectarte e retomar o acceso a túa conta.", + "Sign in and regain access to your account.": "Conéctate e recupera o acceso a túa conta.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Non podes conectar a conta. Contacta coa administración do teu servidor para máis información.", + "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.": "Aviso: os teus datos personais (incluíndo chaves de cifrado) aínda están gardadas nesta sesión. Pechaa se remataches de usar esta sesión, ou se quere conectar con outra conta.", + "Unable to load! Check your network connectivity and try again.": "Non cargou! Comproba a conexión a rede e volta a intentalo.", + "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Hai sesións descoñecidas nesta sala: se continúas sen verificalas será posible para alguén fisgar na túa chamada.", + "Review Sessions": "Revisar Sesións", + "Call failed due to misconfigured server": "Fallou a chamada porque o servidor está mal configurado", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Contacta coa administración do teu servidor (%(homeserverDomain)s) para configurar un servidor TURN para que as chamadas funcionen de xeito fiable.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "De xeito alternativo, podes intentar usar o servidor público turn.matrix.org, pero non é tan fiable, e compartirá o teu enderezo IP con ese servidor. Podes xestionar esto en Axustes.", + "Try using turn.matrix.org": "Inténtao usando turn.matrix.org", + "Replying With Files": "Respondendo con Ficheiros", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Neste intre non é posible responder cun ficheiro. Queres subir este ficheiro sen responder?", + "The file '%(fileName)s' failed to upload.": "Fallou a subida do ficheiro '%(fileName)s'.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "O ficheiro '%(fileName)s' supera o tamaño máximo permitido polo servidor", + "The server does not support the room version specified.": "O servidor non soporta a versión da sala indicada.", + "If you cancel now, you won't complete verifying the other user.": "Se cancelas agora non completarás a verificación da outra usuaria.", + "If you cancel now, you won't complete verifying your other session.": "Se cancelas agora non completarás o proceso de verificación da outra sesión.", + "If you cancel now, you won't complete your operation.": "Se cancelas agora, non completarás a operación.", + "Cancel entering passphrase?": "Cancelar a escrita da frase de paso?", + "Setting up keys": "Configurando as chaves", + "Verify this session": "Verificar esta sesión", + "Encryption upgrade available": "Mellora do cifrado dispoñible", + "Set up encryption": "Configurar cifrado", + "Review where you’re logged in": "Revisar onde estás conectada", + "New login. Was this you?": "Nova conexión. Foches ti?", + "Name or Matrix ID": "Nome ou ID Matrix", + "Identity server has no terms of service": "O servidor de identidade non ten termos dos servizo", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Esta acción precisa acceder ao servidor de indentidade para validar o enderezo de email ou o número de teléfono, pero o servidor non publica os seus termos do servizo.", + "Only continue if you trust the owner of the server.": "Continúa se realmente confías no dono do servidor.", + "Trust": "Confiar", + "%(name)s is requesting verification": "%(name)s está pedindo a verificación", + "Use your account or create a new one to continue.": "Usa a túa conta ou crea unha nova para continuar.", + "Create Account": "Crear conta", + "Custom (%(level)s)": "Personalizado (%(level)s)", + "Failed to invite users to the room:": "Fallo a convidar a persoas a sala:", + "Messages": "Mensaxes", + "Actions": "Accións", + "Other": "Outro", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Anteponse ¯\\_(ツ)_/¯ a mensaxe en texto plano", + "Sends a message as plain text, without interpreting it as markdown": "Envía unha mensaxe como texto plano, sen interpretalo como markdown", + "Sends a message as html, without interpreting it as markdown": "Envía unha mensaxe como html, sen interpretalo como markdown", + "Upgrades a room to a new version": "Subir a sala de versión", + "You do not have the required permissions to use this command.": "Non tes os permisos suficientes para usar este comando.", + "Error upgrading room": "Fallo ao actualizar a sala", + "Double check that your server supports the room version chosen and try again.": "Comproba ben que o servidor soporta a versión da sala escollida e inténtao outra vez.", + "Changes your display nickname in the current room only": "Cambia o teu nome mostrado só para esta esta sala", + "Changes the avatar of the current room": "Cambia o avatar da sala actual", + "Changes your avatar in this current room only": "Cambia o teu avatar só nesta sala", + "Changes your avatar in all rooms": "Cambia o teu avatar en todas as salas", + "Gets or sets the room topic": "Obtén ou establece o asunto da sala", + "Failed to set topic": "Fallo ao establecer asunto", + "This room has no topic.": "Esta sala non ten asunto.", + "Sets the room name": "Establecer nome da sala", + "Use an identity server": "Usar un servidor de identidade", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Usar un servidor de identidade para convidar por email. Preme continuar para usar o servidor de identidade por omisión (%(defaultIdentityServerName)s) ou cambiao en Axustes.", + "Use an identity server to invite by email. Manage in Settings.": "Usar un servidor de indentidade para convidar por email. Xestionao en Axustes.", + "Command failed": "O comando fallou", + "Could not find user in room": "Non se atopa a usuaria na sala", + "Adds a custom widget by URL to the room": "Engade un widget por URL personalizado a sala", + "Please supply a widget URL or embed code": "Proporciona o URL do widget ou incrusta o código", + "Please supply a https:// or http:// widget URL": "Escribe un https:// ou http:// como URL do widget", + "You cannot modify widgets in this room.": "Non podes modificar os widgets desta sala.", + "Unknown (user, session) pair:": "Par descoñecido (usuaria, sesión):", + "Session already verified!": "A sesión xa está verificada!", + "WARNING: Session already verified, but keys do NOT MATCH!": "AVISO: xa está verificada a sesión, pero as chaves NON CONCORDAN!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "AVISO: FALLOU A VERIFICACIÓN DAS CHAVES! A chave de firma para %(userId)s na sesión %(deviceId)s é \"%(fprint)s\" que non concordan coa chave proporcionada \"%(fingerprint)s\". Esto podería significar que as túas comunicacións foron interceptadas!", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "A chave de firma proporcionada concorda coa chave de firma recibida desde a sesión %(deviceId)s de %(userId)s. Sesión marcada como verificada.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Se usas ou non a función 'breadcrumbs ' (avatares enriba da listaxe de salas)", + "Unbans user with given ID": "Desbloquea usuaria co ID dado", + "Verifies a user, session, and pubkey tuple": "Verifica unha usuaria, sesión e chave pública", + "Forces the current outbound group session in an encrypted room to be discarded": "Forza que se descarte a sesión de saída actual nunha sala cifrada", + "Sends the given message coloured as a rainbow": "Envía a mensaxe dada colorida como o arco da vella", + "Sends the given emote coloured as a rainbow": "Envía o emoji colorido como un arco da vella", + "Displays list of commands with usages and descriptions": "Mostra unha listaxe de comandos con usos e descricións", + "Displays information about a user": "Mostra información acerca da usuaria", + "Send a bug report with logs": "Envía un informe de fallos con rexistros", + "Opens chat with the given user": "Abre unha conversa coa usuaria", + "Sends a message to the given user": "Envía unha mensaxe a usuaria", + "%(senderName)s made no change.": "%(senderName)s non fixo cambios.", + "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s cambiou o nome da sala de %(oldRoomName)s a %(newRoomName)s.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s actualizou esta sala.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s converteu en pública a sala para calquera que teña a ligazón.", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s fixo que a sala sexa só por convite.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s cambiou a regra de participación a %(rule)s", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s permite que as convidadas se unan a sala.", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s non permite que as convidadas se unan a sala.", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s cambiou acceso de convidada a %(rule)s", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s activou a popularidade para %(groups)s nesta sala.", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s desactivou a popularidade para %(groups)s nesta sala.", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s activou a popularidade para %(newGroups)s e desactivou a popularidade para %(oldGroups)s nesta sala.", + "Capitalization doesn't help very much": "Escribir con maiúsculas non axuda moito", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Substitucións predecibles como '@' no lugar de 'a' non son de gran axuda", + "Group & filter rooms by custom tags (refresh to apply changes)": "Agrupar e filtrar salas con etiquetas personalizadas (actuliza para aplicar cambios)", + "Enable Community Filter Panel": "Activar o panel de Filtro de comunidades", + "General": "Xeral", + "Discovery": "Descubrir", + "Deactivate account": "Desactivar conta", + "For help with using Riot, click here.": "Para ter axuda con Riot, preme aquí.", + "For help with using Riot, click here or start a chat with our bot using the button below.": "Se precisas axuda usando Riot, preme aquí ou inicia unha conversa co noso bot usando o botón inferior.", + "Help & About": "Axuda & Acerca de", + "Security & Privacy": "Seguridade & Privacidade", + "Where you’re logged in": "Onde estás conectada", + "Change room name": "Cambiar nome da sala", + "Roles & Permissions": "Roles & Permisos", + "Room %(name)s": "Sala %(name)s", + "Recent rooms": "Salas recentes", + "Direct Messages": "Mensaxes Directas", + "Create room": "Crear sala", + "You can use /help to list available commands. Did you mean to send this as a message?": "Podes usar axuda para ver os comandos dispoñibles. ¿Querías mellor enviar esto como unha mensaxe?", + "Error updating flair": "Fallo ao actualizar popularidade", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Algo fallou cando se actualizaba a popularidade da sala. Pode ser un fallo temporal ou que o servidor non o permita.", + "Enter the name of a new server you want to explore.": "Escribe o nome do novo servidor que queres explorar.", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Se hai contexto que cres que axudaría a analizar o problema, como o que estabas a facer, ID da sala, ID da usuaria, etc., por favor inclúeo aquí.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Para evitar informes duplicados, mira os informes existentes primeiro (e engade un +1) ou crea un novo informe se non o atopas.", + "Command Help": "Comando Axuda", + "To help us prevent this in future, please send us logs.": "Para axudarnos a previr esto no futuro, envíanos o rexistro.", + "Help": "Axuda", + "Explore Public Rooms": "Explorar Salas Públicas", + "Explore": "Explorar", + "Filter": "Filtrar", + "Filter rooms…": "Filtrar salas…", + "%(creator)s created and configured the room.": "%(creator)s creou e configurou a sala.", + "Explore rooms": "Explorar salas", + "General failure": "Fallo xeral", + "This homeserver does not support login using email address.": "Este servidor non soporta a conexión usando enderezos de email.", + "Clear room list filter field": "Baleirar o campo do filtro de salas", + "Room name or address": "Nome da sala ou enderezo", + "Joins room with given address": "Unirse a sala co enderezo dado", + "Unrecognised room address:": "Non se recoñece o enderezo da sala:", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s estableceu o enderezo principal da sala %(address)s.", + "%(senderName)s removed the main address for this room.": "%(senderName)s eliminiou o enderezo principal desta sala.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s engadiu os enderezos alternativos %(addresses)s para esta sala.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s engadiu o enderezo alternativo %(addresses)s para esta sala.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s eliminou os enderezos alternativos %(addresses)s desta sala.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s eliminou o enderezo alternativo %(addresses)s desta sala.", + "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s cambiou os enderezos alternativos desta sala.", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s cambiou o enderezo principal e alternativo para esta sala.", + "%(senderName)s changed the addresses for this room.": "%(senderName)s cambiou o enderezo desta sala.", + "%(senderName)s placed a voice call.": "%(senderName)s fixo unha chamada de voz.", + "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s fixo unha chamada de voz. (non soportado neste navegador)", + "%(senderName)s placed a video call.": "%(senderName)s fixo unha chamada de vídeo.", + "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s fixo unha chamada de vídeo. (non soportado por este navegador)", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revogou o convite para que %(targetDisplayName)s se una a esta sala.", + "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s eliminou a regra que bloqueaba usuarias con %(glob)s", + "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s eliminou a regra que bloquea salas con %(glob)s", + "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s eliminou a regra que bloquea servidores con %(glob)s", + "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s eliminou a regra de bloqueo con %(glob)s", + "%(senderName)s updated an invalid ban rule": "%(senderName)s actualizou unha regra de bloqueo non válida", + "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s actualizou a regra que bloquea usuarias con %(glob)s por %(reason)s", + "Show info about bridges in room settings": "Mostrar info sobre pontes nos axustes da sala", + "Room Addresses": "Enderezos da sala", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Algo fallou ao actualizar os enderezos alternativos da sala. É posible que o servidor non o permita ou acontecese un fallo temporal.", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Os enderezos publicados poden ser usados por calquera persoa ou servidor para unirse a sala. Para publicar un enderezo, primeiro debe establecerse como enderezo local.", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Establecer enderezos para a sala para que poida ser atopada no teu servidor local (%(localDomain)s)", + "Room Settings - %(roomName)s": "Axustes da sala - %(roomName)s", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Eliminar o enderezo da sala %(alias)s e eliminar %(name)s do directorio?", + "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s actualizou a regra de bloqueo de salas con %(glob)s por %(reason)s", + "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s actualizou a regra de bloqueo de servidores con %(glob)s por %(reason)s", + "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s actualizou a regra de bloqueo con %(glob)s por %(reason)s", + "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s creou unha regra de bloqueo de usuarias con %(glob)s por %(reason)s", + "You signed in to a new session without verifying it:": "Conectácheste nunha nova sesión sen verificala:", + "Verify your other session using one of the options below.": "Verifica a túa outra sesión usando unha das opcións inferiores.", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) conectouse a unha nova sesión sen verificala:", + "Ask this user to verify their session, or manually verify it below.": "Pídelle a usuaria que verifique a súa sesión, ou verificaa manualmente aquí.", + "Not Trusted": "Non confiable", + "Manually Verify by Text": "Verificar manualmente por texto", + "Interactively verify by Emoji": "Verificar interactivamente por Emoji", + "Done": "Feito", + "%(displayName)s is typing …": "%(displayName)s está escribindo…", + "%(names)s and %(count)s others are typing …|other": "%(names)s e outras %(count)s están escribindo…", + "%(names)s and %(count)s others are typing …|one": "%(names)s e outra están escribindo…", + "%(names)s and %(lastPerson)s are typing …": "%(names)s e %(lastPerson)s están escribindo…", + "Cannot reach homeserver": "Non se acadou o servidor", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Asegúrate de que tes boa conexión a internet, ou contacta coa administración do servidor", + "Your Riot is misconfigured": "O teu Riot está mal configurado", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Pídelle a administración do teu Riot que comprobe a configuración para entradas duplicadas ou incorrectas.", + "Cannot reach identity server": "Non se acadou o servidor de identidade", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Podes rexistrarte, pero algunhas características non estarán dispoñibles ata que o servidor de identidade volte a conectarse. Se segues a ver este aviso, comproba os axustes ou contacta coa administración.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Podes restablecer o contrasinal, pero algunhas características non estarán dispoñibles ata que o servidor de identidade se conecte. Se segues a ver este aviso comproba os axustes ou contacta coa administración.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Podes conectarte, pero algunhas características non estarán dispoñibles ata que o servidor de identidade volte a conectarse. Se segues a ver este aviso, comproba os axustes ou contacta coa administración.", + "No homeserver URL provided": "Non se estableceu URL do servidor", + "Unexpected error resolving homeserver configuration": "Houbo un fallo ao acceder a configuración do servidor", + "Unexpected error resolving identity server configuration": "Houbo un fallo ao acceder a configuración do servidor de identidade", + "The message you are trying to send is too large.": "A mensaxe a enviar é demasiado grande.", + "Unable to connect to Homeserver. Retrying...": "Non se conectou co Servidor. Reintentando...", + "a few seconds ago": "fai uns segundos", + "about a minute ago": "fai un minuto", + "%(num)s minutes ago": "fai %(num)s minutos", + "about an hour ago": "fai unha hora", + "%(num)s hours ago": "fai %(num)s horas", + "about a day ago": "onte", + "%(num)s days ago": "fai %(num)s días", + "a few seconds from now": "hai só uns segundos", + "about a minute from now": "haberá un minuto", + "%(num)s minutes from now": "fará %(num)s minutos", + "about an hour from now": "fará unha hora", + "%(num)s hours from now": "fará %(num)s horas", + "about a day from now": "foi onte", + "%(num)s days from now": "fará %(num)s días", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Unrecognised address": "Enderezo non recoñecible", + "You do not have permission to invite people to this room.": "Non tes permiso para convidar a xente a esta sala.", + "User %(userId)s is already in the room": "A usuaria %(userId)s xa está na sala", + "User %(user_id)s does not exist": "A usuaria %(user_id)s non existe", + "User %(user_id)s may or may not exist": "A usuaria %(user_id)s podería non existir", + "The user must be unbanned before they can be invited.": "A usuria debe ser desbloqueada antes de poder convidala.", + "Messages in this room are end-to-end encrypted.": "As mensaxes desta sala están cifradas de extremo-a-extremo.", + "Messages in this room are not end-to-end encrypted.": "As mensaxes desta sala non están cifradas de extremo-a-extremo.", + "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "As mensaxes desta sala están cifradas de extremo-a-extremo. No perfil da usuaria tes máis info e podes verificala.", + "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s creou unha regra bloqueando salas con %(glob)s por %(reason)s", + "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s creou unha regra bloqueando servidores con %(glob)s por %(reason)s", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s creou unha regra de bloqueo con %(glob)s por %(reason)s", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s cambiou unha regra que bloqueaba usuarias con %(oldGlob)s a %(newGlob)s por %(reason)s", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s cambiou unha regra que bloqueaba salas con %(oldGlob)s a %(newGlob)s por %(reason)s", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s cambiou unha regra que bloqueaba servidores con %(oldGlob)s a %(newGlob)s por %(reason)s", + "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s actualizou unha regra de bloqueo con %(oldGlob)s a %(newGlob)s por %(reason)s", + "The user's homeserver does not support the version of the room.": "O servidor da usuaria non soporta a versión da sala.", + "Unknown server error": "Erro descoñecido no servidor", + "Use a few words, avoid common phrases": "Usa unhas poucas palabras, evita frases comúns", + "No need for symbols, digits, or uppercase letters": "Non precisa símbolos, díxitos ou maiúsculas", + "Use a longer keyboard pattern with more turns": "Usa un patrón de teclado máis grande con algún xiro", + "Avoid repeated words and characters": "Evita palabras repetidas ou caracteres", + "Avoid sequences": "Evita secuencias", + "Avoid recent years": "Evita anos recentes", + "Avoid years that are associated with you": "Evita anos que están asociados a ti", + "Avoid dates and years that are associated with you": "Evita datas e anos que están relacionados contigo", + "All-uppercase is almost as easy to guess as all-lowercase": "As maiúsculas son case tan fáciles de adiviñar como as minúsculas", + "Reversed words aren't much harder to guess": "A escritura inversa non é moi difícil de adiviñar", + "Add another word or two. Uncommon words are better.": "Engade unha ou dúas palabras máis. Mellor que sexan raras.", + "Repeats like \"aaa\" are easy to guess": "Repeticións com \"aaa\" son fáciles de adiviñar", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Repeticións com \"abcabcabc\" son só un pouco máis difíciles de adiviñar que \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Secuencias como abc ou 6543 son fáciles de adiviñar", + "Recent years are easy to guess": "Os anos recentes son fáciles de adiviñar", + "Dates are often easy to guess": "Normalmente as datas son fáciles de adiviñar", + "This is a top-10 common password": "Este é o top-10 dos contrasinais habituais", + "This is a top-100 common password": "Este un top-100 dos contrasinais habituais", + "This is a very common password": "Este é un contrasinal moi común", + "This is similar to a commonly used password": "É semellante a un contrasinal habitualmente utilizado", + "A word by itself is easy to guess": "Por si sola, unha palabra é fácil de adiviñar", + "Names and surnames by themselves are easy to guess": "Nomes e apelidos por si mesmos son fáciles de adiviñar", + "Common names and surnames are easy to guess": "Os nomes e alcumes son fáciles de adiviñar", + "Help us improve Riot": "Axúdanos a mellorar Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Envía datos de uso anónimos que nos axudarán a mellorar Riot. Esto precisa usar unha cookie.", + "I want to help": "Quero axudar", + "No": "Non", + "Verify all your sessions to ensure your account & messages are safe": "Verifica todas as outras sesións para asegurar que a túa conta e mensaxes están seguros", + "Review": "Revisar", + "Later": "Máis tarde", + "Your homeserver has exceeded its user limit.": "O teu servidor superou o seu límite de usuaras.", + "Your homeserver has exceeded one of its resource limits.": "O teu servidor superou un dous seus límites de recursos.", + "Contact your server admin.": "Contacta coa administración.", + "Ok": "Ok", + "Set password": "Establecer contrasinal", + "To return to your account in future you need to set a password": "Para voltar a túa conta no futuro debes establecer un contrasinal", + "Set up": "Configurar", + "Upgrade": "Mellorar", + "Verify": "Verificar", + "Verify yourself & others to keep your chats safe": "Verifica a túa conta e a de outras para ter conversas seguras", + "Other users may not trust it": "Outras usuarias poderían non confiar", + "Verify the new login accessing your account: %(name)s": "Verifica a conexión accedendo a túa conta: %(name)s", + "Restart": "Reiniciar", + "Upgrade your Riot": "Mellora o teu Riot", + "A new version of Riot is available!": "Hai unha nova versión de Riot!", + "There was an error joining the room": "Houbo un fallo ao unirte a sala", + "Font scaling": "Escalado da tipografía", + "Custom user status messages": "Mensaxes de estado personalizados", + "Render simple counters in room header": "Mostrar contadores simples na cabeceira da sala", + "Multiple integration managers": "Múltiples xestores da integración", + "Try out new ways to ignore people (experimental)": "Novos xeitos de ignorar persoas (experimental)", + "Show join/leave messages (invites/kicks/bans unaffected)": "Mostrar mensaxes de entrada/saída (mais non convites/expulsións/bloqueos)", + "Subscribing to a ban list will cause you to join it!": "Subscribíndote a unha lista de bloqueo fará que te unas a ela!", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Aviso: Actualizando a sala non fará que os membros da sala migren automáticamente a nova versión da sala. Publicaremos unha ligazón a nova sala na versión antiga da sala - os membros terán que premer na ligazón para unirse a nova sala.", + "Join the conversation with an account": "Únete a conversa cunha conta", + "Re-join": "Volta a unirte", + "You can only join it with a working invite.": "Só podes unirte cun convite activo.", + "Try to join anyway": "Inténtao igualmente", + "You can still join it because this is a public room.": "Podes unirte porque é unha sala pública.", + "Join the discussion": "Súmate a conversa", + "Do you want to join %(roomName)s?": "Queres unirte a %(roomName)s?", + "You're previewing %(roomName)s. Want to join it?": "Vista previa de %(roomName)s. Queres unirte?", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s non ten vista previa. Queres unirte?", + "Join": "Únete", + "Matrix rooms": "Salas Matrix", + "Join millions for free on the largest public server": "Únete a millóns de persoas gratuitamente no maior servidor público", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se non atopas a sala que buscas, pide un convite ou Crea unha nova sala.", + "Match system theme": "Imitar o aspecto do sistema", + "Invalid theme schema.": "Esquema do decorado incorrecto.", + "Error downloading theme information.": "Erro ao descargar información do decorado.", + "Theme added!": "Decorado engadido!", + "Custom theme URL": "URL do decorado personalizado", + "Add theme": "Engadir decorado", + "Theme": "Decorado", + "Language and region": "Idioma e rexión", + "Your theme": "O teu decorado" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 17254a77db..81526c2c69 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -27,7 +27,7 @@ "No Microphones detected": "Nem található mikrofon", "No Webcams detected": "Nem található webkamera", "No media permissions": "Nincs media jogosultság", - "You may need to manually permit Riot to access your microphone/webcam": "Lehet hogy manuálisan kell engedélyeznie a Riot-nak a hozzáférést a mikrofonhoz ás webkamerához", + "You may need to manually permit Riot to access your microphone/webcam": "Lehet hogy manuálisan kell engedélyezned a Riot-nak a hozzáférést a mikrofonhoz ás webkamerához", "Default Device": "Alapértelmezett eszköz", "Microphone": "Mikrofon", "Camera": "Kamera", @@ -176,10 +176,10 @@ "Local addresses for this room:": "A szoba helyi címe:", "Logout": "Kilép", "Low priority": "Alacsony prioritás", - "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s elérhetővé tette a szoba új üzeneteit nekik minden résztvevő a szobában, amióta meg van hívva.", - "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s elérhetővé tette a szoba új üzeneteit nekik minden résztvevő a szobában, amióta csatlakozott.", - "%(senderName)s made future room history visible to all room members.": "%(senderName)s elérhetővé tette a szoba új üzeneteit nekik minden szoba tagság.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s elérhetővé tette a szoba új üzeneteit nekik bárki.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s láthatóvá tette a szoba új üzeneteit nekik minden szoba tagnak, a meghívásuk idejétől kezdve.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s láthatóvá tette a szoba új üzeneteit minden szoba tagnak, a csatlakozásuk idejétől kezdve.", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s láthatóvá tette a szoba új üzeneteit minden szoba tagnak.", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s mindenki számára láthatóvá tette a szoba új üzeneteit.", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s elérhetővé tette a szoba új üzeneteit nekik ismeretlen (%(visibility)s).", "Manage Integrations": "Integrációk kezelése", "Missing room_id in request": "A kérésből hiányzik a room_id", @@ -1276,11 +1276,11 @@ "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Biztos vagy benne? Ha a kulcsaid nincsenek megfelelően elmentve elveszted a titkosított üzeneteidet.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "A titkosított üzenetek végponttól végpontig titkosítással védettek. Csak neked és a címzetteknek lehet meg a kulcs az üzenet visszafejtéséhez.", "Restore from Backup": "Visszaállítás mentésből", - "Back up your keys before signing out to avoid losing them.": "Ments el a kulcsaidat mielőtt kijelentkezel, hogy ne veszítsd el őket.", + "Back up your keys before signing out to avoid losing them.": "Töltsd fel kulcsaidat a szerverre mielőtt kijelentkezel, hogy ne veszítsd el őket.", "Start using Key Backup": "Kulcs mentés használatának megkezdése", "Never lose encrypted messages": "Soha ne veszíts el titkosított üzenetet", "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "A szobában az üzenetek végponttól végpontig titkosítva vannak. Csak neked és a címzetteknek vannak meg a kulcsok az üzenetek visszafejtéséhez.", - "Securely back up your keys to avoid losing them. Learn more.": "Mentsd el megfelelően a kulcsaidat, hogy ne vesszenek el. Tudj meg róla többet.", + "Securely back up your keys to avoid losing them. Learn more.": "Mentsd el megfelelően a kulcsaidat a szerverre, hogy ne vesszenek el. Tudj meg erről többet.", "Not now": "Most nem", "Don't ask me again": "Ne kérdezz többet", "Nothing appearing? Not all clients support interactive verification yet. .": "Nem jelent meg semmi? Az interaktív hitelesítést még nem minden kliens támogatja. .", @@ -1546,7 +1546,7 @@ "Disconnect": "Kapcsolat bontása", "Identity Server (%(server)s)": "Azonosítás Szerver (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "A kapcsolatok kereséséhez és, hogy megtaláljanak ismerősök ezt a szervert használod: . A használt azonosítási szervert alább tudod megváltoztatni.", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Azonosítási szervert nem használsz jelenleg. Ahhoz, hogy megtalálj ismerősöket vagy téged megtaláljanak ismerősök adj hozzá egyet alább.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy e-mail cím, vagy egyéb azonosító alapján megtalálhassanak az ismerőseid, vagy te megtalálhasd őket, be kell állítanod egy azonosítási szervert.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Ha az azonosítási szerverrel bontod a kapcsolatot az azt fogja eredményezni, hogy más felhasználók nem találnak rád és nem tudsz másokat meghívni e-mail cím vagy telefonszám alapján.", "Enter a new identity server": "Új azonosítási szerver hozzáadása", "Integration Manager": "Integrációs Menedzser", @@ -1819,13 +1819,13 @@ " wants to chat": " csevegni szeretne", "Start chatting": "Beszélgetés elkezdése", "Send cross-signing keys to homeserver": "Kereszt-aláírás kulcsok küldése a matrix szervernek", - "Cross-signing public keys:": "Kereszt-aláírás nyilvános kulcsok:", + "Cross-signing public keys:": "Eszközök közti hitelesítés nyilvános kulcsai:", "not found": "nem található", - "Cross-signing private keys:": "Kereszt-aláírás privát kulcsok:", + "Cross-signing private keys:": "Eszközök közti hitelesítés privát kulcsai:", "in secret storage": "biztonsági tárolóban", "Secret storage public key:": "Biztonsági tároló nyilvános kulcs:", "in account data": "fiók adatokban", - "Cross-signing": "Kereszt-aláírás", + "Cross-signing": "Eszközök közti aláírás", "Enter secret storage passphrase": "Add meg a jelmondatot a biztonsági tárolóhoz", "Unable to access secret storage. Please verify that you entered the correct passphrase.": "A biztonsági tárolóhoz nem lehet hozzáférni. Kérlek ellenőrizd, hogy jó jelmondatot adtál-e meg.", "Warning: You should only access secret storage from a trusted computer.": "Figyelem: Csak biztonságos eszközről férj hozzá a biztonságos tárolóhoz.", @@ -1860,9 +1860,9 @@ "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s megváltoztatta a szabályt amivel szobák voltak kitiltva erről: %(oldGlob)s erre: %(newGlob)s ezért: %(reason)s", "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s megváltoztatta a szabályt amivel szerverek voltak kitiltva erről: %(oldGlob)s erre: %(newGlob)s ezért: %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s megváltoztatta a kitiltó szabályt erről: %(oldGlob)s erre: %(newGlob)s ezért: %(reason)s", - "Cross-signing and secret storage are enabled.": "Kereszt-aláírás és a biztonsági tároló engedélyezve van.", - "Cross-signing and secret storage are not yet set up.": "Kereszt-aláírás és a biztonsági tároló egyenlőre nincs beállítva.", - "Bootstrap cross-signing and secret storage": "Kereszt-aláírás és biztonsági tároló beállítása", + "Cross-signing and secret storage are enabled.": "Eszközök közti hitelesítés és a biztonsági tároló engedélyezve van.", + "Cross-signing and secret storage are not yet set up.": "Az eszközök közti hitelesítés és a biztonsági tároló egyenlőre nincs beállítva.", + "Bootstrap cross-signing and secret storage": "Az eszközök közti hitelesítés és biztonsági tároló beállítása", "not stored": "nincs mentve", "Backup has a valid signature from this user": "A mentés érvényes aláírást tartalmaz a felhasználótól", "Backup has a invalid signature from this user": "A mentés érvénytelen aláírást tartalmaz a felhasználótól", @@ -1989,7 +1989,7 @@ "WARNING: Session already verified, but keys do NOT MATCH!": "FIGYELEM: A munkamenet már ellenőrizve van de a kulcsok NEM EGYEZNEK!", "Enable cross-signing to verify per-user instead of per-session (in development)": "Kereszt-aláírás engedélyezése a felhasználó alapú azonosításhoz a munkamenet alapú helyett (fejlesztés alatt)", "Show padlocks on invite only rooms": "Lakat mutatása azoknál a szobáknál amikbe csak meghívóval lehet belépni", - "Never send encrypted messages to unverified sessions from this session": "Sose küldj titkosított üzenetet nem ellenőrizetlen munkamenetbe ebből a munkamenetből", + "Never send encrypted messages to unverified sessions from this session": "Sose küldj titkosított üzenetet ellenőrizetlen munkamenetbe ebből a munkamenetből", "Never send encrypted messages to unverified sessions in this room from this session": "Sose küldjön titkosított üzeneteket ellenőrizetlen munkamenetekbe ebben a szobában ebből a munkamenetből", "Keep secret storage passphrase in memory for this session": "A biztonsági tároló jelmondatát ebben a munkamenetben tartsa a memóriában", "How fast should messages be downloaded.": "Milyen gyorsan legyenek az üzenetek letöltve.", @@ -2000,7 +2000,7 @@ "They don't match": "Nem egyeznek", "To be secure, do this in person or use a trusted way to communicate.": "A biztonság érdekében ezt végezd el személyesen vagy egy megbízható kommunikációs csatornán.", "Verify yourself & others to keep your chats safe": "Ellenőrizd magad és másokat, hogy a csevegéseid biztonságban legyenek", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "A fiókodhoz tartozik egy kereszt-aláírás a biztonsági tárolóban de ebben a munkamenetben ez még nem megbízható.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "A fiókodhoz tartozik egy eszköz-közti hitelesítési identitás, de ez a munkamenet még nem jelölte megbízhatónak.", "in memory": "memóriában", "Your homeserver does not support session management.": "A matrix szervered nem támogatja a munkamenetek kezelését.", "Unable to load session list": "A munkamenet listát nem lehet betölteni", @@ -2141,10 +2141,10 @@ "Ask this user to verify their session, or manually verify it below.": "Kérd meg a felhasználót, hogy hitelesítse a munkamenetét vagy ellenőrizd kézzel alább.", "Manually Verify": "Manuális ellenőrzés", "Verify by scanning": "Ellenőrzés kód beolvasással", - "Destroy cross-signing keys?": "Eszközök közti hitelesítési kulcsok megsemmisítése?", + "Destroy cross-signing keys?": "Megsemmisíted az eszközök közti hitelesítés kulcsait?", "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Eszközök közti hitelesítési kulcsok törlése végleges. Mindenki akit ezzel hitelesítettél biztonsági figyelmeztetéseket fog látni. Hacsak nem vesztetted el az összes eszközödet amivel eszközök közti hitelesítést tudsz végezni, nem valószínű, hogy ezt szeretnéd tenni.", "Clear cross-signing keys": "Eszközök közti hitelesítési kulcsok törlése", - "Reset cross-signing and secret storage": "Eszközök közti hitelesítés és biztonsági tároló visszaállítása", + "Reset cross-signing and secret storage": "Eszközök közti hitelesítés és biztonsági tároló alaphelyzetbe állítása", "The version of Riot": "Riot verziója", "Whether you're using Riot on a device where touch is the primary input mechanism": "Olyan eszközön használod-e a Riotot, ahol az érintés az elsődleges beviteli mód", "Whether you're using Riot as an installed Progressive Web App": "Progresszív webalkalmazásként használod-e a Riotot", @@ -2170,7 +2170,7 @@ "We recommend you change your password and recovery key in Settings immediately": "Javasoljuk, hogy a jelszavadat és a visszaállítási kulcsodat mihamarabb változtasd meg a Beállításokban", "Order rooms by name": "Szobák rendezése név szerint", "Show rooms with unread notifications first": "Olvasatlan üzeneteket tartalmazó szobák megjelenítése elől", - "Show shortcuts to recently viewed rooms above the room list": "Billentyűkombináció megjelenítése a nemrég meglátogatott szobákhoz a szoba lista felett", + "Show shortcuts to recently viewed rooms above the room list": "Gyorselérési gombok megjelenítése a nemrég meglátogatott szobákhoz a szoba lista felett", "Sign In or Create Account": "Bejelentkezés vagy fiók létrehozása", "Use your account or create a new one to continue.": "A folytatáshoz használd a fiókodat, vagy hozz létre egy újat.", "Create Account": "Fiók létrehozása", @@ -2230,7 +2230,7 @@ "Unverified login. Was this you?": "Ellenőrizetlen bejelentkezés. Te voltál?", "Manually verify all remote sessions": "Az összes távoli munkamenet manuális ellenőrzése", "Update your secure storage": "A biztonsági tárolód frissítése", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "A felhasználó által használt munkamenetek ellenőrzése egyenként, a kereszt-aláírással hitelesített eszközökben nem bízol meg.", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "A felhasználó által használt munkamenetek ellenőrzése egyenként, a eszközök közti aláírással hitelesített eszközökben nem bízol meg.", "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "A nyilvánosságra hozott címeket bárki bármelyik szerveren használhatja a szobádba való belépéshez. A cím közzétételéhez először helyi címnek kell beállítani.", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Állíts be címet ehhez a szobához, hogy a felhasználók a matrix szervereden megtalálhassák (%(localDomain)s)", "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "A titkosított szobákban az üzeneted biztonságban van és csak neked és a címzetteknek van meg az egyedi kulcs a visszafejtéshez.", @@ -2401,7 +2401,7 @@ "Verify all your sessions to ensure your account & messages are safe": "Ellenőrizd minden munkamenetedet, hogy a fiókod és az üzeneteid biztonságban legyenek", "Verify the new login accessing your account: %(name)s": "Ellenőrizd ezt az új bejelentkezést ami hozzáfér a fiókodhoz: %(name)s", "Where you’re logged in": "Ahol be vagy jelentkezve", - "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Állítsd be a munkameneteid neveit, jelentkezz ki vagy ellenőrizd őket a Felhasználói Beállításokban.", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Állítsd be a munkameneteid neveit, jelentkezz ki belőlük, vagy ellenőrizd őket a Felhasználói Beállításokban.", "Restoring keys from backup": "Kulcsok visszaállítása mentésből", "Fetching keys from server...": "Kulcsok lekérdezése a szerverről…", "%(completed)s of %(total)s keys restored": "%(completed)s/%(total)s kulcs visszaállítva", @@ -2424,5 +2424,51 @@ "Click the button below to confirm setting up encryption.": "Az alábbi gomb megnyomásával erősítsd meg, hogy megadod a titkosítási beállításokat.", "Dismiss read marker and jump to bottom": "Az olvasottak jel eltűntetése és ugrás a végére", "Jump to oldest unread message": "A legrégebbi olvasatlan üzenetre ugrás", - "Upload a file": "Fájl feltöltése" + "Upload a file": "Fájl feltöltése", + "Room name or address": "A szoba neve vagy címe", + "Joins room with given address": "Megadott címmel csatlakozik a szobához", + "Unrecognised room address:": "Ismeretlen szoba cím:", + "Font scaling": "Betű nagyítás", + "Use the improved room list (in development - refresh to apply changes)": "Fejlesztett szobalista használata (fejlesztés alatt - frissíts a változások életbe léptetéséhez)", + "Use IRC layout": "IRC kinézet használata", + "Font size": "Betűméret", + "Custom font size": "Egyedit betűméret", + "IRC display name width": "IRC megjelenítési név szélessége", + "Size must be a number": "A méretnek számnak kell lennie", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Az egyedi betűméret csak %(min)s pont és %(max)s pont között lehet", + "Use between %(min)s pt and %(max)s pt": "Csak %(min)s pont és %(max)s pont közötti értéket használj", + "Appearance": "Megjelenítés", + "Help us improve Riot": "Segíts nekünk jobbá tenni a Riotot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Anonim használati adatok küldésével segíthetsz nekünk a Riot fejlesztésében. Ehhez sütiket használ.", + "I want to help": "Segíteni akarok", + "Your homeserver has exceeded its user limit.": "A Matrix szervered túllépte a felhasználói szám korlátot.", + "Your homeserver has exceeded one of its resource limits.": "A Matrix szervered túllépte valamelyik erőforrás korlátját.", + "Contact your server admin.": "Vedd fel a kapcsolatot a szerver gazdájával.", + "Ok": "Rendben", + "Set password": "Jelszó beállítása", + "To return to your account in future you need to set a password": "Ahhoz, hogy a jövőben vissza tudj térni a fiókodba, jelszót kell beállítanod", + "Restart": "Újraindít", + "Upgrade your Riot": "Riot frissítése", + "A new version of Riot is available!": "Új verzió érhető el a Riotból!", + "New version available. Update now.": "Új verzió érhető el.Frissíts most.", + "Please verify the room ID or address and try again.": "Kérlek ellenőrizd a szoba azonosítót vagy címet és próbáld újra.", + "Room ID or address of ban list": "Tiltó lista szoba azonosító vagy cím", + "To link to this room, please add an address.": "Hogy linkelhess egy szobához, adj hozzá egy címet.", + "Create room": "Szoba létrehozása", + "Error creating address": "Cím beállítási hiba", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "A cím beállításánál hiba történt. Vagy nincs engedélyezve a szerveren vagy átmeneti hiba történt.", + "You don't have permission to delete the address.": "A cím törléséhez nincs jogosultságod.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "A cím törlésénél hiba történt. Vagy már nem létezik vagy átmeneti hiba történt.", + "Error removing address": "Cím törlésénél hiba történt", + "Categories": "Kategóriák", + "Room address": "Szoba címe", + "Please provide a room address": "Kérlek add meg a szoba címét", + "This address is available to use": "Ez a cím használható", + "This address is already in use": "Ez a cím már használatban van", + "Set a room address to easily share your room with other people.": "A szoba egyszerű megosztásához másokkal állíts be egy címet.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Ezt a munkamenetet előzőleg egy újabb Riot verzióval használtad. Ahhoz, hogy újra ezt a verziót tudd használni végpontok közötti titkosítással, ki kell lépned majd újra vissza.", + "Address (optional)": "Cím (nem kötelező)", + "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?" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 014d6015b3..e5c1b890d7 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2430,5 +2430,51 @@ "QR Code": "Codice QR", "Dismiss read marker and jump to bottom": "Scarta il segno di lettura e salta alla fine", "Jump to oldest unread message": "Salta al messaggio non letto più vecchio", - "Upload a file": "Invia un file" + "Upload a file": "Invia un file", + "Use IRC layout": "Usa il layout IRC", + "IRC display name width": "Larghezza nome di IRC", + "Create room": "Crea stanza", + "Font scaling": "Ridimensionamento carattere", + "Font size": "Dimensione carattere", + "Custom font size": "Dimensione carattere personalizzata", + "Size must be a number": "La dimensione deve essere un numero", + "Custom font size can only be between %(min)s pt and %(max)s pt": "La dimensione del carattere personalizzata può solo essere tra %(min)s pt e %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Usa tra %(min)s pt e %(max)s pt", + "Appearance": "Aspetto", + "Use the improved room list (in development - refresh to apply changes)": "Usa l'elenco stanze migliorato (in sviluppo - ricarica per applicare le modifiche)", + "Room name or address": "Nome stanza o indirizzo", + "Joins room with given address": "Accede alla stanza con l'indirizzo dato", + "Unrecognised room address:": "Indirizzo stanza non riconosciuto:", + "Please verify the room ID or address and try again.": "Verifica l'ID o l'indirizzo della stanza e riprova.", + "Room ID or address of ban list": "ID o indirizzo stanza della lista ban", + "To link to this room, please add an address.": "Per collegare a questa stanza, aggiungi un indirizzo.", + "Error creating address": "Errore creazione indirizzo", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Si è verificato un errore creando l'indirizzo. Potrebbe non essere permesso dal server o un problema temporaneo.", + "You don't have permission to delete the address.": "Non hai l'autorizzazione per eliminare l'indirizzo.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Si è verificato un errore rimuovendo l'indirizzo. Potrebbe non esistere più o essere un problema temporaneo.", + "Error removing address": "Errore rimozione indirizzo", + "Categories": "Categorie", + "Room address": "Indirizzo stanza", + "Please provide a room address": "Inserisci un indirizzo della stanza", + "This address is available to use": "Questo indirizzo è disponibile per l'uso", + "This address is already in use": "Questo indirizzo è già in uso", + "Set a room address to easily share your room with other people.": "Imposta un indirizzo della stanza per condividerla facilmente con le altre persone.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Hai precedentemente usato una versione più recente di Riot con questa sessione. Per usare ancora questa versione con la cifratura end to end, dovrai disconnetterti e riaccedere.", + "Address (optional)": "Indirizzo (facoltativo)", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Eliminare l'indirizzo della stanza %(alias)s e rimuovere %(name)s dalla cartella?", + "delete the address.": "elimina l'indirizzo.", + "Use a different passphrase?": "Usare una password diversa?", + "Help us improve Riot": "Aiutaci a migliorare Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Invia dati di utilizzo anonimi che ci aiutano a migliorare Riot. Verrà usato un cookie.", + "I want to help": "Voglio aiutare", + "Your homeserver has exceeded its user limit.": "Il tuo homeserver ha superato il limite di utenti.", + "Your homeserver has exceeded one of its resource limits.": "Il tuo homeserver ha superato uno dei suoi limiti di risorse.", + "Contact your server admin.": "Contatta il tuo amministratore del server.", + "Ok": "Ok", + "Set password": "Imposta password", + "To return to your account in future you need to set a password": "Per tornare nel tuo account in futuro, devi impostare una password", + "Restart": "Riavvia", + "Upgrade your Riot": "Aggiorna Riot", + "A new version of Riot is available!": "È disponibile una nuova versione di Riot!", + "New version available. Update now.": "Nuova versione disponibile. Aggiorna ora." } diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 95cd093111..fff38de46e 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -554,7 +554,7 @@ "Download %(text)s": "%(text)s をダウンロード", "Invalid file%(extra)s": "無効なファイル %(extra)s", "Error decrypting image": "イメージの復号化エラー", - "Error decrypting video": "動画の復号化エラー", + "Error decrypting video": "動画の復号エラー", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s が %(roomName)s のアバターを変更しました", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s がルームアバターを削除しました。", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s はルームアバターをに変更しました", @@ -1158,7 +1158,7 @@ "Error updating flair": "バッジの更新でエラーが発生しました。", "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "この部屋のバッジの更新でエラーが発生しました。サーバーが許可していないか、一時的なエラーが発生しました。", "Edited at %(date)s. Click to view edits.": "%(date)sに編集。クリックして編集を表示。", - "edited": "編集済", + "edited": "編集済み", "I don't want my encrypted messages": "暗号化されたメッセージは必要ありません", "Manually export keys": "手動でキーをエクスポート", "You'll lose access to your encrypted messages": "暗号化されたメッセージにアクセスできなくなります", @@ -1371,5 +1371,33 @@ "Riot URL": "Riot URL", "Room ID": "部屋 ID", "Maximize apps": "アプリを最大化する", - "More options": "更なるオプション" + "More options": "更なるオプション", + "Manually verify all remote sessions": "すべてのリモートセッションを手動で検証する", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "クロス署名されたデバイスを信頼せず、信頼済みとしてマークするためにユーザーが使用する各セッションを個別に検証します。", + "This session is backing up your keys. ": "このセッションでは鍵をバックアップしています。 ", + "Show all": "すべて表示", + "Message deleted": "メッセージが削除されました", + "Message deleted by %(name)s": "%(name)s によってメッセージが削除されました", + "Show less": "詳細を非表示", + "Show more": "詳細を表示", + "Backup has a valid signature from this user": "バックアップには、このユーザーによる有効な署名があります", + "Backup has a invalid signature from this user": "バックアップには、このユーザーによる無効な署名があります", + "Backup has a signature from unknown user with ID %(deviceId)s": "バックアップには、ID %(deviceId)s の未知のユーザーによる署名があります", + "Backup has a signature from unknown session with ID %(deviceId)s": "バックアップには、ID %(deviceId)s の未知のセッションによる署名があります", + "Backup has a valid signature from this session": "バックアップには、このセッションによる有効な署名があります", + "Backup has an invalid signature from this session": "バックアップには、このセッションによる無効な署名があります", + "Backup has a valid signature from verified session ": "バックアップには、検証済みのセッション による有効な署名があります", + "Backup has a valid signature from unverified session ": "バックアップには、未検証のセッション による有効な署名があります", + "Backup has an invalid signature from verified session ": "バックアップには、検証済みのセッション による無効な署名があります", + "Backup has an invalid signature from unverified session ": "バックアップには、未検証のセッション による無効な署名があります", + "Backup is not signed by any of your sessions": "バックアップには、あなたのどのセッションからも署名がありません", + "This backup is trusted because it has been restored on this session": "このバックアップは、このセッションで復元されたため信頼されています", + "Where you’re logged in": "現在ログイン中のセッション", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "以下のセッションの公開名を変更したり、サインアウトしたり、あなたのユーザープロフィールからセッションの検証を行うことができます。", + "Enable end-to-end encryption": "エンドツーエンド暗号化を有効にする", + "You can’t disable this later. Bridges & most bots won’t work yet.": "後から無効化することはできません。ブリッジおよびほとんどのボットはまだ動作しません。", + "Use bots, bridges, widgets and sticker packs": "ボット、ブリッジ、ウィジェット、ステッカーパックを使用", + "Service": "サービス", + "Summary": "概要", + "Document": "ドキュメント" } diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 2652433075..d885c6fd7d 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -13,9 +13,9 @@ "The information being sent to us to help make Riot.im better includes:": "Informacija, siunčiama mums, kad padėtų tobulinti Riot.im, apima:", "Fetching third party location failed": "Nepavyko gauti trečios šalies vietos", "A new version of Riot is available.": "Yra prieinama nauja Riot versija.", - "I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti", + "I understand the risks and wish to continue": "Suprantu šią riziką ir noriu tęsti", "Send Account Data": "Siųsti paskyros duomenis", - "Advanced notification settings": "Sudėtingesni pranešimų nustatymai", + "Advanced notification settings": "Išplėstiniai pranešimų nustatymai", "Uploading report": "Išsiunčiama ataskaita", "Sunday": "Sekmadienis", "Guests can join": "Svečiai gali prisijungti", @@ -41,23 +41,23 @@ "Forget": "Pamiršti", "World readable": "Visiems skaitomas", "Mute": "Nutildyti", - "You cannot delete this image. (%(code)s)": "Jūs negalite ištrinti šio paveikslėlio. (%(code)s)", + "You cannot delete this image. (%(code)s)": "Jūs negalite ištrinti šio vaizdo. (%(code)s)", "Cancel Sending": "Atšaukti siuntimą", "Warning": "Įspėjimas", "This Room": "Šis pokalbių kambarys", "Resend": "Siųsti iš naujo", "Room not found": "Kambarys nerastas", "Downloading update...": "Atsiunčiamas atnaujinimas...", - "Messages in one-to-one chats": "Žinutės asmeniniuose pokalbiuose", + "Messages in one-to-one chats": "Žinutės privačiuose pokalbiuose", "Unavailable": "Neprieinamas", - "Error saving email notification preferences": "Klaida, įrašant pranešimų el. paštu nuostatas", + "Error saving email notification preferences": "Klaida išsaugant pranešimų el. paštu nuostatas", "View Decrypted Source": "Peržiūrėti iššifruotą šaltinį", "Failed to update keywords": "Nepavyko atnaujinti raktažodžių", "Notifications on the following keywords follow rules which can’t be displayed here:": "Pranešimai šiems raktažodžiams yra uždrausti taisyklėmis:", "Please set a password!": "Prašau įrašykite slaptažodį!", "powered by Matrix": "veikia su Matrix", "You have successfully set a password!": "Jūs sėkmingai įrašėte slaptažodį!", - "Favourite": "Svarbūs", + "Favourite": "Favoritai", "All Rooms": "Visi pokalbių kambariai", "Explore Room State": "Peržiūrėti kambario būseną", "Source URL": "Šaltinio URL adresas", @@ -65,28 +65,28 @@ "Cancel": "Atšaukti", "Filter results": "Išfiltruoti rezultatus", "Members": "Nariai", - "No update available.": "Nėra prieinamų atnaujinimų.", + "No update available.": "Nėra galimų atnaujinimų.", "Noisy": "Triukšmingas", "Collecting app version information": "Renkama programėlės versijos informacija", "Delete the room alias %(alias)s and remove %(name)s from the directory?": "Ar ištrinti kambarį %(alias)s ir %(name)s kambario pavadinimą iš katalogo?", "Keywords": "Raktažodžiai", "Unpin Message": "Atsegti žinutę", "Enable notifications for this account": "Įjungti pranešimus šiai paskyrai", - "Remove": "Šalinti", + "Remove": "Pašalinti", "Invite to this community": "Pakviesti į šią bendruomenę", "Messages containing keywords": "Žinutės, kuriose yra raktažodžiai", - "When I'm invited to a room": "Kai aš esu pakviestas į pokalbių kambarį", + "When I'm invited to a room": "Kai mane pakviečia į kambarį", "Tuesday": "Antradienis", "Enter keywords separated by a comma:": "Įveskite kableliais atskirtus raktažodžius:", "Search…": "Paieška…", "You have successfully set a password and an email address!": "Jūs sėkmingai įrašėte slaptažodį ir el. pašto adresą!", "Remove %(name)s from the directory?": "Ar ištrinti %(name)s iš katalogo?", - "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot naudoja daug išplėstinių naršyklės funkcionalumų, kai kurie iš jų yra neprieinami ar eksperimentinei Jūsų naršyklėje.", + "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot naudoja daug išplėstinių naršyklės funkcijų, kai kurios iš jų yra neprieinamos arba eksperimentinės jūsų esamoje naršyklėje.", "Event sent!": "Įvykis išsiųstas!", "Unnamed room": "Kambarys be pavadinimo", "Dismiss": "Atmesti", "Explore Account Data": "Peržiūrėti paskyros duomenis", - "Remove from Directory": "Šalinti iš katalogo", + "Remove from Directory": "Pašalinti iš katalogo", "Download this file": "Atsisiųsti šį failą", "Saturday": "Šeštadienis", "Remember, you can always set an email address in user settings if you change your mind.": "Nepamirškite, kad jei persigalvosite, tai bet kada galite nustatyti el. pašto adresą vartotojo nustatymuose.", @@ -103,7 +103,7 @@ "Search": "Ieškoti", "You must specify an event type!": "Privalote nurodyti įvykio tipą!", "(HTTP status %(httpStatus)s)": "(HTTP būsena %(httpStatus)s)", - "Failed to forget room %(errCode)s": "Nepavyko pašalinti pokalbių kambario %(errCode)s", + "Failed to forget room %(errCode)s": "Nepavyko pamiršti kambario %(errCode)s", "What's New": "Kas naujo", "Wednesday": "Trečiadienis", "Send": "Siųsti", @@ -111,8 +111,8 @@ "Send logs": "Siųsti žurnalus", "All messages": "Visos žinutės", "unknown error code": "nežinomas klaidos kodas", - "Call invitation": "Pakvietimas skambinant", - "Messages containing my display name": "Žinutės, kuriose paminėtas mano vardas", + "Call invitation": "Skambučio pakvietimas", + "Messages containing my display name": "Žinutės, kuriose yra mano rodomas vardas", "State Key": "Būklės raktas", "Failed to send custom event.": "Nepavyko išsiųsti pasirinktinio įvykio.", "What's new?": "Kas naujo?", @@ -133,29 +133,29 @@ "Reject": "Atmesti", "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Jūs turbūt juos sukonfigūravote kitoje programėlėje nei Riot. Negalite jų koreguoti Riot programėlėje, bet jie vistiek yra taikomi", "Sorry, your browser is not able to run Riot.": "Atleiskite, jūsų naršyklė negali paleisti Riot.", - "Quote": "Citata", + "Quote": "Cituoti", "Messages in group chats": "Žinutės grupės pokalbiuose", "Yesterday": "Vakar", "Error encountered (%(errorDetail)s).": "Susidurta su klaida (%(errorDetail)s).", - "Low Priority": "Nesvarbūs", + "Low Priority": "Žemo prioriteto", "Riot does not know how to join a room on this network": "Riot nežino kaip prisijungti prie kambario šiame tinkle", "Set Password": "Nustatyti slaptažodį", - "An error occurred whilst saving your email notification preferences.": "Įrašant pranešimų el. paštu nuostatas, įvyko klaida.", + "An error occurred whilst saving your email notification preferences.": "Išsaugant pranešimų el. paštu nuostatas, įvyko klaida.", "Unable to join network": "Nepavyko prisijungti prie tinklo", "Register": "Registruotis", "Off": "Išjungta", "Edit": "Koreguoti", "Mentions only": "Tik paminėjimai", - "remove %(name)s from the directory.": "šalinti %(name)s iš katalogo.", + "remove %(name)s from the directory.": "pašalinti %(name)s iš katalogo.", "You can now return to your account after signing out, and sign in on other devices.": "Po atsijungimo galite grįžti prie savo paskyros ir prisijungti kituose įrenginiuose.", "Continue": "Tęsti", - "Enable email notifications": "Įjungti pranešimus el. paštu", + "Enable email notifications": "Įjungti el. pašto pranešimus", "Event Type": "Įvykio tipas", "No rooms to show": "Nėra kambarių rodymui", "Add rooms to this community": "Įtraukti kambarius į šią bendruomenę", "Pin Message": "Prisegti žinutę", "Failed to change settings": "Nepavyko pakeisti nustatymų", - "Leave": "Atsijungti", + "Leave": "Išeiti", "View Community": "Peržiūrėti bendruomenes", "Developer Tools": "Programuotojo įrankiai", "Unhide Preview": "Rodyti paržiūrą", @@ -164,7 +164,7 @@ "Thank you!": "Ačiū!", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Naudojant šią naršyklę aplikacija gali atrodyti ir reaguoti neteisingai. Kai kurios arba visos funkcijos gali neveikti. Jei vis tiek norite pabandyti gali tęsti, tačiau iškilusios problemos yra jūsų pačių reikalas!", "Checking for an update...": "Tikrinama ar yra atnaujinimų...", - "There are advanced notifications which are not shown here": "Yra išplėstinių pranešimų, kurie nėra čia rodomi", + "There are advanced notifications which are not shown here": "Yra išplėstinių pranešimų, kurie čia nėra rodomi", "e.g. %(exampleValue)s": "pvz., %(exampleValue)s", "e.g. ": "pvz., ", "Your device resolution": "Jūsų įrenginio raiška", @@ -216,7 +216,7 @@ "Failed to add the following rooms to %(groupId)s:": "Nepavyko pridėti šių kambarių į %(groupId)s:", "Riot does not have permission to send you notifications - please check your browser settings": "Riot neturi leidimo siųsti jums pranešimus - patikrinkite savo naršyklės nustatymus", "Riot was not given permission to send notifications - please try again": "Riot nebuvo suteiktas leidimas siųsti pranešimus - bandykite dar kartą", - "Unable to enable Notifications": "Nepavyko įjungti Pranešimus", + "Unable to enable Notifications": "Nepavyko įjungti pranešimų", "This email address was not found": "Šis el. pašto adresas nebuvo rastas", "Admin": "Administratorius", "Start a chat": "Pradėti pokalbį", @@ -239,24 +239,24 @@ "Reason": "Priežastis", "%(targetName)s accepted an invitation.": "%(targetName)s priėmė pakvietimą.", "%(senderName)s invited %(targetName)s.": "%(senderName)s pakvietė %(targetName)s.", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pakeitė savo vardą į %(displayName)s.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nustatė savo vardą į %(displayName)s.", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo vardą (%(oldDisplayName)s).", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pakeitė savo rodomą vardą į %(displayName)s.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nustatė savo rodomą vardą į %(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo rodomą vardą (%(oldDisplayName)s).", "%(senderName)s removed their profile picture.": "%(senderName)s pašalino savo profilio paveikslą.", "%(senderName)s changed their profile picture.": "%(senderName)s pakeitė savo profilio paveikslą.", "%(senderName)s set a profile picture.": "%(senderName)s nustatė profilio paveikslą.", "%(targetName)s rejected the invitation.": "%(targetName)s atmetė pakvietimą.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s pakeitė temą į \"%(topic)s\".", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s pakeitė kambario pavadinimą į %(roomName)s.", - "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s išsiuntė paveikslą.", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s išsiuntė vaizdą.", "Someone": "Kažkas", "%(senderName)s answered the call.": "%(senderName)s atsiliepė į skambutį.", "(unknown failure: %(reason)s)": "(nežinoma klaida: %(reason)s)", "%(senderName)s ended the call.": "%(senderName)s užbaigė skambutį.", "Send anyway": "Vis tiek siųsti", "Unnamed Room": "Bevardis kambarys", - "Show timestamps in 12 hour format (e.g. 2:30pm)": "Rodyti laiko žymas 12 valandų formatu (pvz., 2:30pm)", - "Always show message timestamps": "Visada rodyti žinučių laiko žymas", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Rodyti laiko žymes 12 valandų formatu (pvz. 2:30pm)", + "Always show message timestamps": "Visada rodyti žinučių laiko žymes", "Always show encryption icons": "Visada rodyti šifravimo piktogramas", "Room Colour": "Kambario spalva", "Decline": "Atmesti", @@ -284,17 +284,17 @@ "Drop File Here": "Vilkite failą čia", "Drop file here to upload": "Norėdami įkelti, vilkite failą čia", " (unsupported)": " (nepalaikoma)", - "%(senderName)s sent an image": "%(senderName)s išsiuntė paveikslą", + "%(senderName)s sent an image": "%(senderName)s išsiuntė vaizdą", "%(senderName)s sent a video": "%(senderName)s išsiuntė vaizdo įrašą", "%(senderName)s uploaded a file": "%(senderName)s įkėlė failą", - "Options": "Parametrai", + "Options": "Parinktys", "Key request sent.": "Rakto užklausa išsiųsta.", "device id: ": "įrenginio id: ", "Failed to mute user": "Nepavyko nutildyti naudotoją", "Are you sure?": "Ar tikrai?", "Ignore": "Ignoruoti", "Invite": "Pakviesti", - "User Options": "Naudotojo parametrai", + "User Options": "Vartotojo parinktys", "Admin Tools": "Administratoriaus įrankiai", "Attachment": "Priedas", "Voice call": "Balso skambutis", @@ -309,10 +309,10 @@ "Loading...": "Įkeliama...", "Pinned Messages": "Prisegtos žinutės", "Unknown": "Nežinoma", - "Save": "Įrašyti", + "Save": "Išsaugoti", "(~%(count)s results)|other": "(~%(count)s rezultatų(-ai))", "(~%(count)s results)|one": "(~%(count)s rezultatas)", - "Upload avatar": "Įkelti avatarą", + "Upload avatar": "Įkelti pseudoportretą", "Settings": "Nustatymai", "Community Invites": "Bendruomenės pakvietimai", "%(roomName)s does not exist.": "%(roomName)s neegzistuoja.", @@ -324,7 +324,7 @@ "Anyone who knows the room's link, including guests": "Bet kas, žinantis kambario nuorodą, įskaitant svečius", "Anyone": "Bet kas", "Permissions": "Leidimai", - "Advanced": "Sudėtingesni nustatymai", + "Advanced": "Išplėstiniai", "Add a topic": "Pridėti temą", "Local addresses for this room:": "Vietiniai šio kambario adresai:", "This room has no local addresses": "Šis kambarys neturi jokių vietinių adresų", @@ -337,7 +337,7 @@ "Error decrypting attachment": "Klaida iššifruojant priedą", "Decrypt %(text)s": "Iššifruoti %(text)s", "Download %(text)s": "Atsisiųsti %(text)s", - "Error decrypting image": "Klaida iššifruojant paveikslą", + "Error decrypting image": "Klaida iššifruojant vaizdą", "Error decrypting video": "Klaida iššifruojant vaizdo įrašą", "Copied!": "Nukopijuota!", "Failed to copy": "Nepavyko nukopijuoti", @@ -353,11 +353,11 @@ "The phone number field must not be blank.": "Telefono numerio laukas negali būti tuščias.", "The password field must not be blank.": "Slaptažodžio laukas negali būti tuščias.", "Email address": "El. pašto adresas", - "Remove from community": "Šalinti iš bendruomenės", - "Remove this user from community?": "Šalinti šį naudotoją iš bendruomenės?", - "Failed to remove user from community": "Nepavyko pašalinti naudotoją iš bendruomenės", + "Remove from community": "Pašalinti iš bendruomenės", + "Remove this user from community?": "Pašalinti šį vartotoją iš bendruomenės?", + "Failed to remove user from community": "Nepavyko pašalinti vartotojo iš bendruomenės", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Ar tikrai norite pašalinti \"%(roomName)s\" iš %(groupId)s?", - "Failed to remove room from community": "Nepavyko pašalinti kambarį iš bendruomenės", + "Failed to remove room from community": "Nepavyko pašalinti kambario iš bendruomenės", "Failed to remove '%(roomName)s' from %(groupId)s": "Nepavyko pašalinti \"%(roomName)s\" iš %(groupId)s", "Something went wrong!": "Kažkas nutiko!", "Visibility in Room List": "Matomumas kambarių sąraše", @@ -367,7 +367,7 @@ "Allow": "Leisti", "Delete Widget": "Ištrinti valdiklį", "Delete widget": "Ištrinti valdiklį", - "Failed to remove widget": "Nepavyko pašalinti valdiklį", + "Failed to remove widget": "Nepavyko pašalinti valdiklio", "Scroll to bottom of page": "Slinkti į puslapio apačią", "%(count)s of your messages have not been sent.|other": "Kai kurios iš jūsų žinučių nebuvo išsiųstos.", "%(count)s of your messages have not been sent.|one": "Jūsų žinutė nebuvo išsiųsta.", @@ -375,8 +375,8 @@ "Sent messages will be stored until your connection has returned.": "Išsiųstos žinutės bus saugomos tol, kol atsiras ryšys.", "Active call": "Aktyvus skambutis", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "Čia daugiau nieko nėra! Ar norėtumėte pakviesti kitus ar išjungti įspėjimą apie tuščią kambarį?", - "You seem to be uploading files, are you sure you want to quit?": "Atrodo, kad jūs įkelinėjate failus, ar tikrai norite išeiti?", - "You seem to be in a call, are you sure you want to quit?": "Atrodo, kad dalyvaujate skambutyje, ar tikrai norite išeiti?", + "You seem to be uploading files, are you sure you want to quit?": "Panašu, kad jūs įkelinėjate failus, ar tikrai norite išeiti?", + "You seem to be in a call, are you sure you want to quit?": "Panašu, kad jūs dalyvaujate skambutyje, ar tikrai norite išeiti?", "Search failed": "Paieška nepavyko", "Server may be unavailable, overloaded, or search timed out :(": "Gali būti, kad serveris neprieinamas, perkrautas arba pasibaigė paieškai skirtas laikas :(", "No more results": "Daugiau nėra jokių rezultatų", @@ -395,11 +395,11 @@ "Light theme": "Šviesi tema", "Dark theme": "Tamsi tema", "Success": "Pavyko", - "Unable to remove contact information": "Nepavyko pašalinti kontaktinę informaciją", + "Unable to remove contact information": "Nepavyko pašalinti kontaktinės informacijos", "": "", "Check for update": "Tikrinti, ar yra atnaujinimų", "Reject all %(invitedRooms)s invites": "Atmesti visus %(invitedRooms)s pakvietimus", - "You may need to manually permit Riot to access your microphone/webcam": "Jums gali tekti rankiniu būdu leisti Riot prieigą prie savo mikrofono/kameros", + "You may need to manually permit Riot to access your microphone/webcam": "Jums gali tekti rankiniu būdu duoti leidimą Riot prieigai prie mikrofono/kameros", "No Audio Outputs detected": "Neaptikta jokių garso išvesčių", "No Microphones detected": "Neaptikta jokių mikrofonų", "No Webcams detected": "Neaptikta jokių kamerų", @@ -418,11 +418,11 @@ "A new password must be entered.": "Privalo būti įvestas naujas slaptažodis.", "New passwords must match each other.": "Nauji slaptažodžiai privalo sutapti.", "I have verified my email address": "Aš patvirtinau savo el. pašto adresą", - "Return to login screen": "Grįžti į prisijungimo ekraną", + "Return to login screen": "Grįžti į prisijungimą", "Send Reset Email": "Siųsti atstatymo el. laišką", "Incorrect username and/or password.": "Neteisingas vartotojo vardas ir/arba slaptažodis.", - "Please note you are logging into the %(hs)s server, not matrix.org.": "Turėkite omenyje, kad jūs prisijungiate prie %(hs)s serverio, o ne matrix.org.", - "Failed to fetch avatar URL": "Nepavyko gauti avataro URL", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Atkreipkite dėmesį, kad jūs jungiatės prie %(hs)s serverio, o ne matrix.org.", + "Failed to fetch avatar URL": "Nepavyko gauti pseudoportreto URL", "Commands": "Komandos", "Results from DuckDuckGo": "Rezultatai iš DuckDuckGo", "Notify the whole room": "Pranešti visam kambariui", @@ -523,13 +523,13 @@ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s pakeitė prisegtas kambario žinutes.", "Sorry, your homeserver is too old to participate in this room.": "Atleiskite, jūsų serverio versija yra per sena dalyvauti šiame kambaryje.", "Please contact your homeserver administrator.": "Prašome susisiekti su savo serverio administratoriumi.", - "Enable inline URL previews by default": "Įjungti tiesiogines URL nuorodų peržiūras pagal numatymą", + "Enable inline URL previews by default": "Įjungti URL nuorodų peržiūras kaip numatytasias", "Enable URL previews for this room (only affects you)": "Įjungti URL nuorodų peržiūras šiame kambaryje (įtakoja tik jus)", - "Enable URL previews by default for participants in this room": "Įjungti URL nuorodų peržiūras pagal numatymą dalyviams šiame kambaryje", + "Enable URL previews by default for participants in this room": "Įjungti URL nuorodų peržiūras kaip numatytasias šiame kambaryje esantiems dalyviams", "Confirm password": "Patvirtinkite slaptažodį", "Demote yourself?": "Pažeminti save?", "Demote": "Pažeminti", - "Share Link to User": "Dalintis nuoroda į naudotoją", + "Share Link to User": "Dalintis nuoroda į vartotoją", "Direct chats": "Privatūs pokalbiai", "The conversation continues here.": "Pokalbis tęsiasi čia.", "Jump to message": "Pereiti prie žinutės", @@ -539,10 +539,10 @@ "Who can read history?": "Kas gali skaityti istoriją?", "Only room administrators will see this warning": "Šį įspėjimą matys tik kambario administratoriai", "Remote addresses for this room:": "Nuotoliniai šio kambario adresai:", - "You have enabled URL previews by default.": "Jūs esate įjungę URL nuorodų peržiūras pagal numatymą.", - "You have disabled URL previews by default.": "Jūs esate išjungę URL nuorodų peržiūras pagal numatymą.", - "URL previews are enabled by default for participants in this room.": "URL nuorodų peržiūros yra įjungtos pagal numatymą šio kambario dalyviams.", - "URL previews are disabled by default for participants in this room.": "URL nuorodų peržiūros yra išjungtos pagal numatymą šio kambario dalyviams.", + "You have enabled URL previews by default.": "Jūs esate įjungę URL nuorodų peržiūras kaip numatytasias.", + "You have disabled URL previews by default.": "Jūs esate išjungę URL nuorodų peržiūras kaip numatytasias.", + "URL previews are enabled by default for participants in this room.": "URL nuorodų peržiūros yra įjungtos kaip numatytasios šio kambario dalyviams.", + "URL previews are disabled by default for participants in this room.": "URL nuorodų peržiūros yra išjungtos kaip numatytosios šio kambario dalyviams.", "Invalid file%(extra)s": "Neteisingas failas %(extra)s", "This room is a continuation of another conversation.": "Šis kambarys yra kito pokalbio pratęsimas.", "Click here to see older messages.": "Spustelėkite čia, norėdami matyti senesnes žinutes.", @@ -555,8 +555,8 @@ "No results": "Jokių rezultatų", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s pasikeitė vardą", - "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s pasikeitė avatarą", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s pasikeitė avatarą", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s pasikeitė pseudoportretą", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s pasikeitė pseudoportretą", "collapse": "suskleisti", "expand": "išskleisti", "Room directory": "Kambarių katalogas", @@ -599,7 +599,7 @@ "This homeserver has hit its Monthly Active User limit.": "Šis serveris pasiekė savo mėnesinį aktyvių naudotojų limitą.", "This homeserver has exceeded one of its resource limits.": "Šis serveris viršijo vieno iš savo išteklių limitą.", "Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie serverio. Bandoma iš naujo...", - "Enable widget screenshots on supported widgets": "Palaikomuose valdikliuose įjungti valdiklių ekrano kopijas", + "Enable widget screenshots on supported widgets": "Įjungti valdiklių ekrano kopijas palaikomuose valdikliuose", "Export E2E room keys": "Eksportuoti E2E kambario raktus", "Last seen": "Paskutinį kartą matytas", "Unignore": "Nebeignoruoti", @@ -611,20 +611,20 @@ "System Alerts": "Sistemos įspėjimai", "Failed to unban": "Nepavyko atblokuoti", "not specified": "nenurodyta", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Šifruotuose kambariuose, tokiuose kaip šis, URL nuorodų peržiūra pagal numatymą yra išjungta, kad būtų užtikrinta, jog jūsų namų serveris (kuriame yra generuojamos peržiūros) negalės rinkti informacijos apie šiame kambaryje matomas nuorodas.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s pakeitė %(roomName)s avatarą", - "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s pašalino kambario avatarą.", - "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s pakeitė kambario avatarą į ", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Šifruotuose kambariuose, tokiuose kaip šis, URL nuorodų peržiūros pagal numatymą yra išjungtos, kad būtų užtikrinta, jog jūsų serveris (kur yra generuojamos peržiūros) negali rinkti informacijos apie jūsų šiame kambaryje peržiūrėtas nuorodas.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s pakeitė kambario %(roomName)s pseudoportretą", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s pašalino kambario pseudoportretą.", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s pakeitė kambario pseudoportretą į ", "Removed or unknown message type": "Žinutė pašalinta arba yra nežinomo tipo", "Filter community members": "Filtruoti bendruomenės dalyvius", "Removing a room from the community will also remove it from the community page.": "Pašalinus kambarį iš bendruomenės, taip pat pašalins jį iš bendruomenės puslapio.", "Filter community rooms": "Filtruoti bendruomenės kambarius", "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Padėkite patobulinti Riot.im, siųsdami anoniminius naudojimosi duomenis. Tai panaudos slapuką (žiūrėkite mūsų Slapukų politiką).", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Padėkite patobulinti Riot.im, siųsdami anoniminius naudojimosi duomenis. Tai panaudos slapuką.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Padėkite patobulinti Riot.im, siųsdami anoniminius naudojimosi duomenis. Tai naudos slapuką.", "Please contact your service administrator to get this limit increased.": "Norėdami padidinti šį limitą, susisiekite su savo paslaugų administratoriumi.", "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "Šis namų serveris pasiekė savo mėnesinį aktyvių naudotojų limitą, taigi, kai kurie naudotojai negalės prisijungti.", "This homeserver has exceeded one of its resource limits so some users will not be able to log in.": "Šis namų serveris viršijo vieno iš savo išteklių limitą, taigi, kai kurie naudotojai negalės prisijungti.", - "An error ocurred whilst trying to remove the widget from the room": "Įvyko klaida, bandant pašalinti valdiklį iš kambario", + "An error ocurred whilst trying to remove the widget from the room": "Bandant pašalinti valdiklį iš kambario įvyko klaida", "Blacklist": "Blokuoti", "Unblacklist": "Atblokuoti", "Verify...": "Patvirtinti...", @@ -636,12 +636,12 @@ "%(oneUser)sleft %(count)s times|one": "%(oneUser)s išėjo", "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s pasikeitė vardus", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s pasikeitė vardą %(count)s kartų(-us)", - "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s pasikeitė avatarą %(count)s kartų(-us)", - "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s pasikeitė avatarą %(count)s kartų(-us)", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s pasikeitė pseudoportretus %(count)s kartų(-us)", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s pasikeitė pseudoportretą %(count)s kartų(-us)", "And %(count)s more...|other": "Ir dar %(count)s...", "Existing Call": "Esamas skambutis", "A call is already in progress!": "Skambutis jau vyksta!", - "Default": "Numatytasis", + "Default": "Numatytas", "Restricted": "Apribotas", "Moderator": "Moderatorius", "Ignores a user, hiding their messages from you": "Ignoruoja vartotoją, slepiant nuo jūsų jo žinutes", @@ -659,7 +659,7 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nustatė pagrindinį šio kambario adresą į %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s pašalino pagrindinį šio kambario adresą.", "Disinvite": "Atšaukti pakvietimą", - "Disinvite this user?": "Atšaukti pakvietimą šiam naudotojui?", + "Disinvite this user?": "Atšaukti pakvietimą šiam vartotojui?", "Unknown for %(duration)s": "Nežinoma jau %(duration)s", "Unable to load! Check your network connectivity and try again.": "Nepavyko įkelti! Patikrinkite savo tinklo ryšį ir bandykite dar kartą.", "%(targetName)s joined the room.": "%(targetName)s prisijungė prie kambario.", @@ -697,15 +697,15 @@ "Algorithm: ": "Algoritmas: ", "Don't ask again": "Daugiau nebeklausti", "Set up": "Nustatyti", - "Publish this room to the public in %(domain)s's room directory?": "Paskelbti šį kambarį į viešąjį %(domain)s kambarių katalogą?", + "Publish this room to the public in %(domain)s's room directory?": "Paskelbti šį kambarį viešai %(domain)s kambarių kataloge?", "Start authentication": "Pradėti tapatybės nustatymą", "Failed to load group members": "Nepavyko įkelti grupės dalyvių", - "Manage Integrations": "Tvarkyti integracijas", + "Manage Integrations": "Valdyti integracijas", "Matrix Room ID": "Matrix kambario ID", "That doesn't look like a valid email address": "Tai nepanašu į teisingą el. pašto adresą", "Preparing to send logs": "Ruošiamasi išsiųsti žurnalus", "Incompatible Database": "Nesuderinama duomenų bazė", - "Deactivate Account": "Pasyvinti paskyrą", + "Deactivate Account": "Deaktyvuoti paskyrą", "I verify that the keys match": "Aš patvirtinu, kad raktai sutampa", "Incompatible local cache": "Nesuderinamas vietinis podėlis", "Updating Riot": "Atnaujinama Riot", @@ -725,7 +725,7 @@ "No backup found!": "Nerasta jokios atsarginės kopijos!", "Backup Restored": "Atsarginė kopija atkurta", "Failed to decrypt %(failedCount)s sessions!": "Nepavyko iššifruoti %(failedCount)s seansų!", - "Next": "Kitas", + "Next": "Toliau", "Private Chat": "Privatus pokalbis", "Public Chat": "Viešas pokalbis", "There are no visible files in this room": "Šiame kambaryje nėra matomų failų", @@ -740,7 +740,7 @@ "Your Communities": "Jūsų bendruomenės", "Create a new community": "Sukurti naują bendruomenę", "You have no visible notifications": "Jūs neturite matomų pranešimų", - "Failed to perform homeserver discovery": "Nepavyko atlikti namų serverio aptikimo", + "Failed to perform homeserver discovery": "Nepavyko atlikti serverio radimo", "Error: Problem communicating with the given homeserver.": "Klaida: Problemos susisiekiant su nurodytu namų serveriu.", "This server does not support authentication with a phone number.": "Šis serveris nepalaiko tapatybės nustatymo telefono numeriu.", "Great! This passphrase looks strong enough.": "Puiku! Ši slapta frazė atrodo pakankamai stipri.", @@ -757,7 +757,7 @@ "Your Riot is misconfigured": "Jūsų Riot yra neteisingai sukonfigūruotas", "Sign in to your Matrix account on %(serverName)s": "Prisijunkite prie savo Matrix paskyros %(serverName)s serveryje", "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Nepriklausomai nuo to ar jūs naudojate 'duonos trupinių' funkciją (avatarai virš kambarių sąrašo)", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Nepriklausomai nuo to ar jūs naudojate 'duonos trupinių' funkciją (pseudoportretai virš kambarių sąrašo)", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur šis puslapis įtraukia identifikuojamą informaciją, kaip kambarys, vartotojas ar grupės ID, tie duomenys yra pašalinami prieš siunčiant į serverį.", "The remote side failed to pick up": "Nuotolinėi pusėi nepavyko atsiliepti", "Call failed due to misconfigured server": "Skambutis nepavyko dėl neteisingai sukonfigūruoto serverio", @@ -775,7 +775,7 @@ "Invite new community members": "Pakviesti naujus bendruomenės narius", "Name or Matrix ID": "Vardas arba Matrix ID", "Identity server has no terms of service": "Tapatybės serveris neturi paslaugų teikimo sąlygų", - "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Šiam veiksmui reikalinga prieiti numatytąjį tapatybės serverį , kad patvirtinti el. pašto adresą arba telefono numerį, bet serveris neturi jokių paslaugos teikimo sąlygų.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Šiam veiksmui reikalinga pasiekti numatytąjį tapatybės serverį , kad patvirtinti el. pašto adresą arba telefono numerį, bet serveris neturi jokių paslaugos teikimo sąlygų.", "Only continue if you trust the owner of the server.": "Tęskite tik tada, jei pasitikite serverio savininku.", "Trust": "Pasitikėti", "Failed to invite users to the room:": "Nepavyko pakviesti vartotojų į kambarį:", @@ -790,14 +790,14 @@ "You do not have the required permissions to use this command.": "Jūs neturite reikalingų leidimų naudoti šią komandą.", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Įspėjimas: Kambario atnaujinimas automatiškai nemigruos kambario dalyvių į naują kambario versiją. Mes paskelbsime nuorodą į naują kambarį senojoje kambario versijoje - kambario dalyviai turės ją paspausti, norėdami prisijungti prie naujo kambario.", "Changes your display nickname in the current room only": "Pakeičia jūsų rodomą slapyvardį tik esamame kambaryje", - "Changes the avatar of the current room": "Pakeičia esamo kambario avatarą", - "Changes your avatar in this current room only": "Pakeičia jūsų avatarą tik esamame kambaryje", - "Changes your avatar in all rooms": "Pakeičia jūsų avatarą visuose kambariuose", + "Changes the avatar of the current room": "Pakeičia esamo kambario pseudoportretą", + "Changes your avatar in this current room only": "Pakeičia jūsų pseudoportretą tik esamame kambaryje", + "Changes your avatar in all rooms": "Pakeičia jūsų pseudoportretą visuose kambariuose", "Gets or sets the room topic": "Gauna arba nustato kambario temą", "This room has no topic.": "Šis kambarys neturi temos.", "Sets the room name": "Nustato kambario pavadinimą", "Use an identity server": "Naudoti tapatybės serverį", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tam, kad būtų naudojamas numatytasis tapatybės serveris %(defaultIdentityServerName)s, spauskite tęsti, arba tvarkykite nustatymuose.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tam, kad toliau būtų naudojamas numatytasis tapatybės serveris %(defaultIdentityServerName)s, spauskite tęsti, arba tvarkykite nustatymuose.", "Use an identity server to invite by email. Manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tvarkykite nustatymuose.", "Joins room with given alias": "Prisijungia prie kambario su nurodytu slapyvardžiu", "Unbans user with given ID": "Atblokuoja vartotoją su nurodytu id", @@ -831,8 +831,8 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s išsiuntė pakvietimą %(targetDisplayName)s prisijungti prie kambario.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams, nuo pat jų prisijungimo.", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s padarė būsimą kambario istoriją matomą nežinomam (%(visibility)s).", - "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s iš %(fromPowerLevel)s į %(toPowerLevel)s", - "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s pakeitė %(powerLevelDiffText)s galios lygį.", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s galios lygį iš %(fromPowerLevel)s į %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s pakeitė %(powerLevelDiffText)s.", "%(displayName)s is typing …": "%(displayName)s rašo …", "%(names)s and %(count)s others are typing …|other": "%(names)s ir %(count)s kiti rašo …", "%(names)s and %(count)s others are typing …|one": "%(names)s ir dar vienas rašo …", @@ -874,7 +874,7 @@ "Do you want to chat with %(user)s?": "Ar jūs norite kalbėtis su %(user)s?", " wants to chat": " nori kalbėtis", "Start chatting": "Pradėti kalbėtis", - "Do you want to join %(roomName)s?": "Ar jūs norite prisijungti prie %(roomName)s?", + "Do you want to join %(roomName)s?": "Ar jūs norite prisijungti prie %(roomName)s kanalo?", " invited you": " jus pakvietė", "You're previewing %(roomName)s. Want to join it?": "Jūs peržiūrite %(roomName)s. Norite prie jo prisijungti?", "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s negali būti peržiūrėtas. Ar jūs norite prie jo prisijungti?", @@ -896,10 +896,10 @@ "Explore": "Žvalgyti", "Filter": "Filtruoti", "Filter rooms…": "Filtruoti kambarius…", - "This room is not public. You will not be able to rejoin without an invite.": "Šis kambarys nėra viešas. Jūs negalėsite prie jo vėl prisijungti be pakvietimo.", - "Are you sure you want to leave the room '%(roomName)s'?": "Ar tikrai norite palikti kambarį %(roomName)s?", + "This room is not public. You will not be able to rejoin without an invite.": "Šis kambarys nėra viešas. Jūs negalėsite vėl prie jo prisijungti be pakvietimo.", + "Are you sure you want to leave the room '%(roomName)s'?": "Ar tikrai norite išeiti iš kambario %(roomName)s?", "%(creator)s created and configured the room.": "%(creator)s sukūrė ir sukonfigūravo kambarį.", - "Riot failed to get the public room list.": "Riot nepavyko gauti viešų kambarių sąrašą.", + "Riot failed to get the public room list.": "Riot nepavyko gauti viešų kambarių sąrašo.", "General failure": "Bendras triktis", "Messages containing my username": "Žinutės, kuriose yra mano vartotojo vardas", "Set a new account password...": "Nustatyti naują paskyros slaptažodį...", @@ -925,7 +925,7 @@ "Create your account": "Sukurkite savo paskyrą", "Change identity server": "Pakeisti tapatybės serverį", "Change": "Keisti", - "Change room avatar": "Keisti kambario avatarą", + "Change room avatar": "Keisti kambario pseudoportretą", "Change room name": "Keisti kambario pavadinimą", "Change main address for the room": "Keisti pagrindinį kambario adresą", "Change history visibility": "Keisti istorijos matomumą", @@ -939,12 +939,12 @@ "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Ar tikrai norite pašalinti (ištrinti) šį įvykį? Atkreipkite dėmesį į tai, kad jei jūs ištrinsite kambario pavadinimo arba temos keitimo įvykį, tai gali atšaukti patį pakeitimą.", "We recommend you change your password and recovery key in Settings immediately": "Mes rekomenduojame nedelsiant Nustatymuose pasikeisti jūsų slaptažodį ir atgavimo raktą", "Email (optional)": "El. paštas (neprivaloma)", - "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jei jūs nenustatėte naujo paskyros atgavimo metodo, tada gali būti, kad užpuolikas bando patekti į jūsų paskyrą. Nedelsiant Nustatymuose pakeiskite savo paskyros slaptažodį ir nustatykite naują paskyros atgavimo metodą.", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jei jūs nepašalinote paskyros atgavimo metodo, tada gali būti, kad užpuolikas bando patekti į jūsų paskyrą. Nedelsiant Nustatymuose pakeiskite savo paskyros slaptažodį ir nustatykite naują paskyros atgavimo metodą.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jei jūs nenustatėte naujo paskyros atgavimo metodo, gali būti, kad užpuolikas bando patekti į jūsų paskyrą. Nedelsiant nustatymuose pakeiskite savo paskyros slaptažodį ir nustatykite naują atgavimo metodą.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jei jūs nepašalinote paskyros atgavimo metodo, gali būti, kad užpuolikas bando patekti į jūsų paskyrą. Nedelsiant nustatymuose pakeiskite savo paskyros slaptažodį ir nustatykite naują atgavimo metodą.", "Help & About": "Pagalba ir Apie", "Direct Messages": "Privačios žinutės", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Nustatykite adresus šiam kambariui, kad vartotojai galėtų surasti šį kambarį per jūsų serverį (%(localDomain)s)", - "Direct message": "Privati žinutė", + "Direct message": "Siųsti tiesioginę žinutę", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s prisijungė %(count)s kartų(-us)", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s prisijungė", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s prisijungė %(count)s kartų(-us)", @@ -957,10 +957,10 @@ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s išėjo ir vėl prisijungė", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s išėjo ir vėl prisijungė %(count)s kartų(-us)", "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s išėjo ir vėl prisijungė", - "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s atmetė jų kvietimus %(count)s kartų(-us)", - "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s atmetė jų kvietimus", - "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s atmetė jų kvietimą %(count)s kartų(-us)", - "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s atmetė jų kvietimą", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s atmetė pakvietimus %(count)s kartų(-us)", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s atmetė pakvietimus", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s atmetė pakvietimą %(count)s kartų(-us)", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s atmetė pakvietimą", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s atšaukė savo pakvietimus %(count)s kartų(-us)", "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s atšaukė savo pakvietimus", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s atšaukė savo pakvietimą %(count)s kartų(-us)", @@ -990,7 +990,7 @@ "Custom level": "Pritaikytas lygis", "Can't find this server or its room list": "Negalime rasti šio serverio arba jo kambarių sąrašo", "Matrix rooms": "Matrix kambariai", - "Recently Direct Messaged": "Neseniai siųsta privati žinutė", + "Recently Direct Messaged": "Neseniai tiesiogiai susirašyta", "Command Help": "Komandų pagalba", "Help": "Pagalba", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Sukurkite bendruomenę, kad kartu sugrupuotumėte vartotojus ir kambarius! Sukurkite pagrindinį puslapį, kad pažymėtumėte savo vietą Matrix visatoje.", @@ -1002,7 +1002,7 @@ "Failed to set direct chat tag": "Nepavyko nustatyti privataus pokalbio žymos", "Navigation": "Navigacija", "Calls": "Skambučiai", - "Room List": "Kambarių sąrašas", + "Room List": "Kambarių Sąrašas", "Autocomplete": "Autorašymas", "Alt": "Alt", "Alt Gr": "Alt Gr", @@ -1013,18 +1013,18 @@ "If you cancel now, you won't complete verifying your other session.": "Jei atšauksite dabar, neužbaigsite kito seanso patvirtinimo.", "Verify this session": "Patvirtinti šį seansą", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s pakeitė kambario pavadinimą iš %(oldRoomName)s į %(newRoomName)s.", - "Show display name changes": "Rodyti vardo pakeitimus", + "Show display name changes": "Rodyti rodomo vardo pakeitimus", "Show read receipts sent by other users": "Rodyti kitų vartotojų siųstus perskaitymo kvitus", "Order rooms by name": "Rūšiuoti kambarius pagal pavadinimą", "The other party cancelled the verification.": "Kita šalis atšaukė patvirtinimą.", "Public Name": "Viešas Vardas", - "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Užšifruotos žinutės yra apsaugotos su \"end-to-end\" šifravimu. Tik jūs ir gavėjas(-ai) turi raktus šioms žinutėms perskaityti.", - "Back up your keys before signing out to avoid losing them.": "Prieš atsijungdami sukurkite atsarginę savo raktų kopiją, kad jų neprarastumėte.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Užšifruotos žinutės yra apsaugotos visapusiu šifravimu. Tik jūs ir gavėjas(-ai) turi raktus šioms žinutėms perskaityti.", + "Back up your keys before signing out to avoid losing them.": "Prieš atsijungdami sukurkite atsarginę savo raktų kopiją, kad išvengtumėte jų praradimo.", "Start using Key Backup": "Pradėti naudoti Atsarginę Raktų Kopiją", "Display Name": "Rodomas Vardas", "Please verify the room ID or alias and try again.": "Prašome patikrinti kambario ID arba slapyvardį ir bandyti dar kartą.", "Room %(name)s": "Kambarys %(name)s", - "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Atnaujinimas išjungs dabartinę kambario instanciją ir sukurs atnaujintą kambarį tuo pačiu vardu.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Atnaujinimas išjungs dabartinę kambario instanciją ir sukurs atnaujintą kambarį tuo pačiu pavadinimu.", "Other published addresses:": "Kiti paskelbti adresai:", "No other published addresses yet, add one below": "Kol kas nėra kitų paskelbtų adresų, pridėkite vieną žemiau", "Room Name": "Kambario Pavadinimas", @@ -1049,8 +1049,8 @@ "Create a private room": "Sukurti privatų kambarį", "Name": "Pavadinimas", "Topic (optional)": "Tema (nebūtina)", - "Hide advanced": "Paslėpti sudėtingesnius nustatymus", - "Show advanced": "Rodyti sudėtingesnius nustatymus", + "Hide advanced": "Paslėpti išplėstinius", + "Show advanced": "Rodyti išplėstinius", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Neleisti kitų matrix serverių vartotojams prisijungti prie šio kambario (Šis nustatymas negali būti vėliau pakeistas!)", "Session name": "Seanso pavadinimas", "Session key": "Seanso raktas", @@ -1058,13 +1058,13 @@ "You'll lose access to your encrypted messages": "Jūs prarasite prieigą prie savo užšifruotų žinučių", "New session": "Naujas seansas", "Enter secret storage passphrase": "Įveskite slaptos saugyklos slaptafrazę", - "Enter recovery passphrase": "Įveskite atstatymo slaptafrazę", + "Enter recovery passphrase": "Įveskite atgavimo slaptafrazę", "Warning: you should only set up key backup from a trusted computer.": "Įspėjimas: atsarginę raktų kopiją sukurkite tik iš patikimo kompiuterio.", "Warning: You should only set up key backup from a trusted computer.": "Įspėjimas: Atsarginę raktų kopiją sukurkite tik iš patikimo kompiuterio.", "Server Name": "Serverio Pavadinimas", "Other servers": "Kiti serveriai", "Add room": "Sukurti kambarį", - "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Slaptažodžio keitimas ištrins visų jūsų seansų šifravimo raktus, todėl nebebus galima perskaityti užšifruotos pokalbių istorijos. Nustatykite Raktų Atsarginę Kopiją arba eksportuokite savo kambarių raktus iš kito seanso prieš atstatydami slaptažodį.", + "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Slaptažodžio keitimas ištrins visų jūsų seansų šifravimo raktus, todėl nebebus galima perskaityti užšifruotos pokalbių istorijos. Sukurkite atsarginę raktų kopiją arba eksportuokite savo kambarių raktus iš kito seanso prieš atstatydami slaptažodį.", "Set a display name:": "Nustatyti rodomą vardą:", "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Apsaugokite savo šifravimo raktus slaptafraze. Maksimaliam saugumui užtikrinti ji turi skirtis nuo jūsų paskyros slaptažodžio:", "Enter a passphrase": "Įveskite slaptafrazę", @@ -1073,5 +1073,520 @@ "For maximum security, this should be different from your account password.": "Maksimaliam saugumui užtikrinti ji turi skirtis nuo jūsų paskyros slaptažodžio.", "Enter a passphrase...": "Įveskite slaptafrazę...", "Please enter your passphrase a second time to confirm.": "Įveskite slaptafrazę antrą kartą, kad ją patvirtintumėte.", - "Secure your backup with a passphrase": "Apsaugokite savo atsarginę kopiją slaptafraze" + "Secure your backup with a passphrase": "Apsaugokite savo atsarginę kopiją slaptafraze", + "Set up encryption": "Nustatyti šifravimą", + "COPY": "Kopijuoti", + "Enter recovery key": "Įveskite atgavimo raktą", + "Keep going...": "Tęskite...", + "Please install Chrome, Firefox, or Safari for the best experience.": "Riot geriausiai veikia su Chrome, Firefox, arba Safari naršyklėmis.", + "Syncing...": "Sinchronizuojama...", + "Signing In...": "Prijungiama...", + "If you've joined lots of rooms, this might take a while": "Jei esate prisijungę prie daug kambarių, tai gali užtrukti", + "Without completing security on this session, it won’t have access to encrypted messages.": "Neužbaigus saugumo šiame seanse, jis neturės prieigos prie šifruotų žinučių.", + "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Nustatykite atgavimo slaptafrazę, kad apsaugotumėte šifruotą informaciją ir atgautumėte ją jei atsijungsite. Ji turi skirtis nuo jūsų paskyros slaptažodžio:", + "Enter a recovery passphrase": "Įveskite atgavimo slaptafrazę", + "Back up encrypted message keys": "Padaryti atsargines šifruotų žinučių raktų kopijas", + "Set up with a recovery key": "Nustatyti su atgavimo raktu", + "Enter your recovery passphrase a second time to confirm it.": "Įveskite atgavimo slaptafrazę antrą kartą, kad ją patvirtintumėte.", + "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Jūsų atgavimo raktas yra atsarginė saugumo priemonė - jūs galite jį naudoti prieigos prie jūsų šifruotų žinučių atgavimui, jei pamiršite savo atgavimo slaptafrazę.", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Laikykite šio rakto kopiją saugioje vietoje, pavyzdžiui slaptažodžių tvarkyklėje arba seife.", + "Your recovery key": "Jūsų atgavimo raktas", + "Copy": "Kopijuoti", + "Make a copy of your recovery key": "Padaryti atgavimo rakto kopiją", + "Please enter your recovery passphrase a second time to confirm.": "Įveskite atgavimo slaptafrazę antrą kartą, kad patvirtintumėte.", + "Later": "Vėliau", + "Verify yourself & others to keep your chats safe": "Patvirtinkite save ir kitus, kad jūsų pokalbiai būtų saugūs", + "Go back": "Grįžti", + "This room is end-to-end encrypted": "Šis kambarys visapusiškai užšifruotas", + "Send a message…": "Siųsti žinutę…", + "Never lose encrypted messages": "Niekada nepraraskite šifruotų žinučių", + "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Žinutės šiame kambaryje yra apsaugotos visapusiu šifravimu. Tik jūs ir gavėjas(-ai) turite raktus šioms žinutėms perskaityti.", + "Securely back up your keys to avoid losing them. Learn more.": "Saugiai sukurkite jūsų raktų atsarginę kopiją, kad išvengtumėte jų praradimo. Sužinoti daugiau.", + "Not now": "Ne dabar", + "Don't ask me again": "Daugiau neklausti", + "Send as message": "Siųsti kaip žinutę", + "Messages in this room are end-to-end encrypted.": "Žinutės šiame kambaryje yra visapusiškai užšifruotos.", + "Messages in this room are not end-to-end encrypted.": "Žinutės šiame kambaryje nėra visapusiškai užšifruotos.", + "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Žinutės šiame kambaryje yra visapusiškai užšifruotos. Sužinokite daugiau ir patvirtinkite vartotojus jų profilyje.", + "Confirm Removal": "Patvirtinkite pašalinimą", + "Manually export keys": "Eksportuoti raktus rankiniu būdu", + "Send a Direct Message": "Siųsti tiesioginę žinutę", + "Go Back": "Grįžti", + "Go back to set it again.": "Grįžti atgal, kad nustatyti iš naujo.", + "Click the button below to confirm adding this email address.": "Paspauskite mygtuką žemiau, kad patvirtintumėte šio el. pašto pridėjimą.", + "Add an email address to configure email notifications": "Pridėkite el. pašto adresą, kad nustatytumėte el. pašto pranešimus", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Rekomenduojame, prieš atsijungiant, iš tapatybės serverio pašalinti savo el. pašto adresus ir telefono numerius.", + "Email addresses": "El. pašto adresai", + "Account management": "Paskyros valdymas", + "Deactivating your account is a permanent action - be careful!": "Paskyros deaktyvavimas yra neatšaukiamas veiksmas - būkite atsargūs!", + "Your email address hasn't been verified yet": "Jūsų el. pašto adresas dar nebuvo patvirtintas", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Išsiuntėme jums el. laišką, kad patvirtintumėme jūsų adresą. Sekite ten esančiais nurodymais ir tada paspauskite žemiau esantį mygtuką.", + "Email Address": "El. pašto adresas", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "Jūs galite naudoti pasirinktinius serverio nustatymus, kad prisijungtumėte prie kitų Matrix serverių, nurodydami kito serverio URL. Tai leidžia jums naudotis šia programa su kitame serveryje esančia Matrix paskyra.", + "Enter your custom homeserver URL What does this mean?": "Įveskite pasirinktinio serverio URL Ką tai reiškia?", + "Homeserver URL": "Serverio URL", + "Homeserver URL does not appear to be a valid Matrix homeserver": "Serverio adresas neatrodo esantis tinkamas Matrix serveris", + "This homeserver does not support login using email address.": "Šis serveris nepalaiko prisijungimo naudojant el. pašto adresą.", + "Review Sessions": "Peržiūrėti seansus", + "Setting up keys": "Raktų nustatymas", + "Review where you’re logged in": "Peržiūrėkite kur esate prisijungę", + "Verify all your sessions to ensure your account & messages are safe": "Patvirtinkite visus savo seansus, kad užtikrintumėte savo paskyros ir žinučių saugumą", + "Review": "Peržiūrėti", + "Message deleted": "Žinutė ištrinta", + "Message deleted by %(name)s": "Žinutė, ištrinta %(name)s", + "Warning: You should only do this on a trusted computer.": "Įspėjimas: Tai atlikite tik saugiame kompiuteryje.", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Pasiekite savo saugių žinučių istoriją ir kryžminio pasirašymo tapatybę, naudojamą kitų seansų patvirtinimui, įvesdami savo atgavimo slaptafrazę.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options.": "Jei pamiršote savo atgavimo slaptafrazę jūs galite naudoti savo atgavimo raktą arba nustatyti naujus atgavimo nustatymus.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Jei pamiršote savo atgavimo slaptafrazę jūs galite naudoti savo atgavimo raktą arba nustatyti naujus atgavimo nustatymus", + "Confirm your identity by entering your account password below.": "Patvirtinkite savo tapatybę žemiau įvesdami savo paskyros slaptažodį.", + "Use an email address to recover your account": "Naudokite el. pašto adresą, kad prireikus galėtumėte atgauti paskyrą", + "Passwords don't match": "Slaptažodžiai nesutampa", + "Use lowercase letters, numbers, dashes and underscores only": "Naudokite tik mažąsias raides, brūkšnelius ir pabraukimus", + "Great! This recovery passphrase looks strong enough.": "Puiku! Ši slaptafrazė atrodo pakankamai stipri.", + "That matches!": "Tai sutampa!", + "That doesn't match.": "Tai nesutampa.", + "Confirm your recovery passphrase": "Patvirtinti atgavimo slaptafrazę", + "Your recovery key has been copied to your clipboard, paste it to:": "Jūsų atgavimo raktas buvo nukopijuotas į jūsų iškarpinę, jūs galite:", + "Your recovery key is in your Downloads folder.": "Jūsų atgavimo raktas yra Parsisiuntimų kataloge.", + "Print it and store it somewhere safe": "Atsispausdinti jį ir laikyti saugioje vietoje", + "Save it on a USB key or backup drive": "Išsaugoti jį USB rakte arba atsarginių kopijų diske", + "Copy it to your personal cloud storage": "Nukopijuoti jį į savo asmeninę debesų saugyklą", + "You can now verify your other devices, and other users to keep your chats safe.": "Jūs dabar galite patvirtinti kitus savo įrenginius ir kitus vartotojus, kad jūsų pokalbiai būtų saugūs.", + "Confirm recovery passphrase": "Patvirtinkite atgavimo slaptafrazę", + "You're done!": "Atlikta!", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Pakeitus slaptažodį šiuo metu, visuose seansuose bus anuliuoti visapusio šifravimo raktai, tad šifruotų pokalbių istorija taps neperskaitoma, nebent jūs eksportuosite savo kambarių raktus ir po to importuosite juos atgal. Ateityje ši funkcija bus pataisyta.", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Jūsų slaptažodis buvo sėkmingai pakeistas. Jūs kituose seansuose negausite pranešimų, kol iš naujo prie jų neprisijungsite", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "El. laiškas buvo išsiųstas į %(emailAddress)s. Kai paspausite jame esančią nuorodą, tada spauskite žemiau.", + "Your password has been reset.": "Jūsų slaptažodis buvo iš naujo nustatytas.", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Jūs buvote atjungtas iš visų seansų ir toliau nebegausite pranešimų. Tam, kad vėl įjungtumėte pranešimus, iš naujo prisijunkite kiekviename įrenginyje.", + "Show more": "Rodyti daugiau", + "Log in to your new account.": "Prisijunkite prie naujos paskyros.", + "Registration Successful": "Registracija sėkminga", + "Welcome to %(appName)s": "Sveiki prisijungę į %(appName)s", + "Liberate your communication": "Išlaisvinkite savo bendravimą", + "Explore Public Rooms": "Žvalgyti viešus kambarius", + "Create a Group Chat": "Sukurti grupės pokalbį", + "New login. Was this you?": "Naujas prisijungimas. Ar tai jūs?", + "%(senderName)s placed a voice call.": "%(senderName)s pradėjo balso skambutį.", + "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s pradėjo vaizdo skambutį. (nepalaikoma šios naršyklės)", + "%(senderName)s placed a video call.": "%(senderName)s pradėjo vaizdo skambutį.", + "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s pradėjo vaizdo skambutį. (nepalaikoma šios naršyklės)", + "Verify your other session using one of the options below.": "Patvirtinkite savo kitą seansą naudodami vieną iš žemiau esančių parinkčių.", + "Messages containing @room": "Žinutės, kuriose yra @kambarys", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Laukiama, kol jūsų kitas seansas, %(deviceName)s (%(deviceId)s), patvirtins…", + "Waiting for your other session to verify…": "Laukiama, kol jūsų kitas seansas patvirtins…", + "To be secure, do this in person or use a trusted way to communicate.": "Norėdami būti saugūs, darykite tai asmeniškai arba naudodamiesi patikimu bendravimo būdu.", + "Verify": "Patvirtinti", + "Verify the new login accessing your account: %(name)s": "Patvirtinkite naują prisijungimą prie jūsų paskyros: %(name)s", + "Confirm deleting these sessions": "Patvirtinkite šių seansų ištrinimą", + "Delete sessions|other": "Ištrinti seansus", + "Delete sessions|one": "Ištrinti seansą", + "Delete %(count)s sessions|other": "Ištrinti %(count)s seansus(-ų)", + "Delete %(count)s sessions|one": "Ištrinti %(count)s seansą", + "ID": "ID", + "Restore from Backup": "Atkurti iš atsarginės kopijos", + "Flair": "Ženkliukai", + "Access Token:": "Prieigos talonas:", + "Preferences": "Nuostatos", + "Cryptography": "Kriptografija", + "Security & Privacy": "Saugumas ir Privatumas", + "Voice & Video": "Garsas ir Vaizdas", + "Enable room encryption": "Įjungti kambario šifravimą", + "Enable encryption?": "Įjungti šifravimą?", + "Encryption": "Šifravimas", + "Once enabled, encryption cannot be disabled.": "Įjungus šifravimą jo nebus galima išjungti.", + "Who can access this room?": "Kas turi prieigą prie šio kambario?", + "Join as voice or video.": "Prisijungti kaip balsas arba vaizdas.", + "Deactivate user?": "Deaktyvuoti vartotoją?", + "Deactivate user": "Deaktyvuoti vartotoją", + "Failed to deactivate user": "Nepavyko deaktyvuoti vartotojo", + "Sessions": "Seansai", + "Try again later, or ask a room admin to check if you have access.": "Pabandykite vėliau arba paprašykite kambario administratoriaus patikrinti, ar turite prieigą.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "Bandant patekti į kambarį buvo gauta klaida: %(errcode)s. Jei manote, kad matote šį pranešimą per klaidą, prašome apie ją pranešti.", + "Error updating flair": "Klaida atnaujinant ženkliuką", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Įvyko klaida atnaujinant ženkliukus šiam kambariui. Serveris gali jų neleisti arba įvyko laikina klaida.", + "Showing flair for these communities:": "Ženkliukai rodomi šioms bendruomenėms:", + "This room is not showing flair for any communities": "Šis kambarys nerodo ženkliukų jokioms bendruomenėms", + "Waiting for you to accept on your other session…": "Laukiama kol jūs priimsite kitame savo seanse…", + "Start Verification": "Pradėti patvirtinimą", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Šifruotuose kambariuose jūsų žinutės yra apsaugotos ir tik jūs ir gavėjas turite unikalius raktus joms atrakinti.", + "Verify User": "Patvirtinti vartotoją", + "For extra security, verify this user by checking a one-time code on both of your devices.": "Dėl papildomo saugumo patvirtinkite šį vartotoją patikrindami vienkartinį kodą abiejuose jūsų įrenginiuose.", + "%(count)s verified sessions|other": "%(count)s patvirtintų seansų", + "Hide verified sessions": "Slėpti patvirtintus seansus", + "%(count)s sessions|other": "%(count)s seansai(-ų)", + "Hide sessions": "Slėpti seansus", + "%(role)s in %(roomName)s": "%(role)s kambaryje %(roomName)s", + "Security": "Saugumas", + "Verify by scanning": "Patvirtinti nuskaitant", + "Verify all users in a room to ensure it's secure.": "Patvirtinkite visus vartotojus kambaryje, kad užtikrintumėte jo saugumą.", + "You cancelled verification on your other session.": "Jūs atšaukėte patvirtinimą kitame savo seanse.", + "%(displayName)s cancelled verification.": "%(displayName)s atšaukė patvirtinimą.", + "You cancelled verification.": "Jūs atšaukėte patvirtinimą.", + "Verification cancelled": "Patvirtinimas atšauktas", + "Encryption enabled": "Šifravimas įjungtas", + "Encryption not enabled": "Šifravimas neįjungtas", + "Display your community flair in rooms configured to show it.": "Rodyti savo bendruomenės ženkliukus kambariuose, kuriuose nustatytas jų rodymas.", + "You're not currently a member of any communities.": "Jūs šiuo metu nesate jokios bendruomenės narys.", + "More options": "Daugiau parinkčių", + "Are you sure you want to remove %(serverName)s": "Ar tikrai norite pašalinti %(serverName)s", + "Enable end-to-end encryption": "Įjungti visapusį šifravimą", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Jūs negalėsite vėliau to išjungti. Tiltai ir dauguma bot'ų dar nėra palaikomi.", + "Are you sure you want to deactivate your account? This is irreversible.": "Ar tikrai norite deaktyvuoti savo paskyrą? Tai yra negrįžtama.", + "Verify session": "Patvirtinti seansą", + "Verify by comparing a short text string.": "Patvirtinkite palygindami trumpą teksto eilutę.", + "To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:": "Norėdami patvirtinti, kad šis seansas yra patikimas, susisiekite su jo savininku kokiu nors kitu būdu (pvz.: asmeniškai arba telefono skambučiu) ir paklauskite ar jų Vartotojo Nustatymuose matomas šio seanso raktas sutampa su raktu esančiu žemiau:", + "Start verification": "Pradėti patvirtinimą", + "Are you sure you want to sign out?": "Ar tikrai norite atsijungti?", + "Use this session to verify your new one, granting it access to encrypted messages:": "Panaudoti šį seansą naujo patvirtinimui, suteikant jam prieigą prie šifruotų žinučių:", + "If you didn’t sign in to this session, your account may be compromised.": "Jei jūs nesijungėte prie šios sesijos, jūsų paskyra gali būti sukompromituota.", + "This wasn't me": "Tai ne aš", + "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Jei jūs susidūrėte su klaidomis arba norėtumėte palikti atsiliepimą, praneškite mums GitHub'e.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Tam, kad būtų išvengta pasikartojančių problemų, pirmiausia peržiūrėkite esamas problemas (ir pridėkite +1) arba, jei nerandate, sukurkite naują svarstomą problemą.", + "Report bugs & give feedback": "Pranešti apie klaidas ir palikti atsiliepimą", + "Report Content to Your Homeserver Administrator": "Pranešti apie turinį serverio administratoriui", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Pranešant apie šią netinkamą žinutę, serverio administratoriui bus nusiųstas unikalus 'įvykio ID'. Jei žinutės šiame kambaryje yra šifruotos, serverio administratorius negalės perskaityti žinutės teksto ar peržiūrėti failų arba paveikslėlių.", + "Send report": "Siųsti pranešimą", + "Unknown sessions": "Nežinomi seansai", + "Verify other session": "Patvirtinti kitą seansą", + "Are you sure you want to reject the invitation?": "Ar tikrai norite atmesti pakvietimą?", + "Share Permalink": "Dalintis nuoroda", + "Report Content": "Pranešti", + "Nice, strong password!": "Puiku, stiprus slaptažodis!", + "Old cryptography data detected": "Aptikti seni kriptografijos duomenys", + "Verify this login": "Patvirtinti šį prisijungimą", + "Registration has been disabled on this homeserver.": "Registracija šiame serveryje išjungta.", + "You can now close this window or log in to your new account.": "Jūs galite uždaryti šį langą arba prisijungti į savo naują paskyrą.", + "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Patvirtinkite savo tapatybę verifikuodami šį prisijungimą viename iš kitų jūsų seansų, suteikdami jam prieigą prie šifruotų žinučių.", + "This requires the latest Riot on your other devices:": "Tam reikia naujausios Riot versijos kituose jūsų įrenginiuose:", + "or another cross-signing capable Matrix client": "arba kitą kryžminį pasirašymą palaikantį Matrix klientą", + "Use Recovery Passphrase or Key": "Naudoti atgavimo slaptafrazę arba raktą", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Atnaujinkite šį seansą, kad jam būtų leista patvirtinti kitus seansus, suteikiant jiems prieigą prie šifruotų žinučių ir juos pažymint kaip patikimus kitiems vartotojams.", + "Use Single Sign On to continue": "Norėdami tęsti naudokite Vieno Prisijungimo sistemą", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Patvirtinkite šio el. pašto adreso pridėjimą naudodami Vieno Prisijungimo sistemą, patvirtinančią jūsų tapatybę.", + "Single Sign On": "Vieno Prisijungimo sistema", + "Confirm adding email": "Patvirtinkite el. pašto pridėjimą", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Patvirtinkite šio telefono numerio pridėjimą naudodami Vieno Prisijungimo sistemą, patvirtinančią jūsų tapatybę.", + "Confirm adding phone number": "Patvirtinkite telefono numerio pridėjimą", + "Click the button below to confirm adding this phone number.": "Paspauskite žemiau esantį mygtuką, kad patvirtintumėte šio numerio pridėjimą.", + "The version of Riot": "Riot versija", + "Match system theme": "Suderinti su sistemos tema", + "Identity Server URL must be HTTPS": "Tapatybės serverio URL privalo būti HTTPS", + "Not a valid Identity Server (status code %(code)s)": "Netinkamas tapatybės serveris (statuso kodas %(code)s)", + "Could not connect to Identity Server": "Nepavyko prisijungti prie tapatybės serverio", + "Disconnect from the identity server and connect to instead?": "Atsijungti nuo tapatybės serverio ir jo vietoje prisijungti prie ?", + "Terms of service not accepted or the identity server is invalid.": "Nesutikta su paslaugų teikimo sąlygomis arba tapatybės serveris yra klaidingas.", + "The identity server you have chosen does not have any terms of service.": "Jūsų pasirinktas tapatybės serveris neturi jokių paslaugų teikimo sąlygų.", + "Disconnect identity server": "Atjungti tapatybės serverį", + "Disconnect from the identity server ?": "Atsijungti nuo tapatybės serverio ?", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Prieš atsijungdami jūs turėtumėte pašalinti savo asmeninius duomenis iš tapatybės serverio . Deja, tapatybės serveris šiuo metu yra išjungtas arba nepasiekiamas.", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "patikrinkite ar tarp jūsų naršyklės įskiepių nėra nieko kas galėtų blokuoti tapatybės serverį (pavyzdžiui \"Privacy Badger\")", + "contact the administrators of identity server ": "susisiekite su tapatybės serverio administratoriais", + "You are still sharing your personal data on the identity server .": "Jūs vis dar dalijatės savo asmeniniais duomenimis tapatybės serveryje .", + "Identity Server (%(server)s)": "Tapatybės serveris (%(server)s)", + "Enter a new identity server": "Pridėkite naują tapatybės serverį", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Naudokite integracijų valdiklį (%(serverName)s) botų, valdiklių ir lipdukų valdymui.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Naudokite integracijų valdiklį botų, valdiklių ir lipdukų valdymui.", + "Manage integrations": "Valdyti integracijas", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų valdikliai gauna konfigūracijos duomenis ir jūsų vardu gali keisti valdiklius, siųsti kambario pakvietimus ir nustatyti galios lygius.", + "Invalid theme schema.": "Klaidinga temos schema.", + "Error downloading theme information.": "Klaida parsisiunčiant temos informaciją.", + "Theme added!": "Tema pridėta!", + "Custom theme URL": "Pasirinktinės temos URL", + "Add theme": "Pridėti temą", + "Theme": "Tema", + "Phone numbers": "Telefono numeriai", + "Language and region": "Kalba ir regionas", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Sutikite su tapatybės serverio (%(serverName)s) paslaugų teikimo sąlygomis, kad leistumėte kitiems rasti jus pagal el. pašto adresą ar telefono numerį.", + "Discovery": "Radimas", + "Discovery options will appear once you have added an email above.": "Radimo parinktys atsiras jums aukščiau pridėjus el. pašto adresą.", + "Unable to revoke sharing for phone number": "Neina atšaukti telefono numerio bendrinimo", + "Unable to share phone number": "Neina bendrinti telefono numerio", + "Unable to verify phone number.": "Neina patvirtinti telefono numerio.", + "Discovery options will appear once you have added a phone number above.": "Radimo parinktys atsiras jums aukščiau pridėjus telefono numerį.", + "Phone Number": "Telefono Numeris", + "Room Topic": "Kambario Tema", + "Your theme": "Jūsų tema", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Valdiklio ištrinimas pašalina jį visiems kambaryje esantiems vartotojams. Ar tikrai norite ištrinti šį valdiklį?", + "Enable 'Manage Integrations' in Settings to do this.": "Įjunkite 'Valdyti integracijas' nustatymuose, kad tai atliktumėte.", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Jūsų Riot neleidžia jums naudoti integracijų valdiklio tam atlikti. Susisiekite su administratoriumi.", + "Enter phone number (required on this homeserver)": "Įveskite telefono numerį (privaloma šiame serveryje)", + "Doesn't look like a valid phone number": "Tai nepanašu į veikiantį telefono numerį", + "Invalid homeserver discovery response": "Klaidingas serverio radimo atsakas", + "Invalid identity server discovery response": "Klaidingas tapatybės serverio radimo atsakas", + "The phone number entered looks invalid": "Įvestas telefono numeris atrodo klaidingas", + "Double check that your server supports the room version chosen and try again.": "Dar kartą įsitikinkite, kad jūsų serveris palaiko pasirinktą kambario versiją ir bandykite iš naujo.", + "Whether you're using Riot on a device where touch is the primary input mechanism": "Nesvarbu, ar naudojate „Riot“ įrenginyje, kuriame pagrindinis įvesties mechanizmas yra lietimas", + "Session already verified!": "Seansas jau patvirtintas!", + "WARNING: Session already verified, but keys do NOT MATCH!": "ĮSPĖJIMAS: Seansas jau patvirtintas, bet raktai NESUTAMPA!", + "Enable cross-signing to verify per-user instead of per-session": "Įjunkite kryžminį pasirašymą, kad patvirtintumėte vartotoją, o ne seansą", + "Enable Emoji suggestions while typing": "Įjungti jaustukų pasiūlymus rašant", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Rodyti priminimą įjungti saugų žinučių atgavimą šifruotuose kambariuose", + "Enable automatic language detection for syntax highlighting": "Įjungti automatinį kalbos aptikimą sintaksės paryškinimui", + "Enable big emoji in chat": "Įjungti didelius jaustukus pokalbiuose", + "Enable Community Filter Panel": "Įjungti bendruomenės filtrų skydelį", + "Enable message search in encrypted rooms": "Įjungti žinučių paiešką užšifruotuose kambariuose", + "Verified!": "Patvirtinta!", + "You've successfully verified this user.": "Jūs sėkmingai patvirtinote šį vartotoją.", + "Got It": "Supratau", + "Verify this session by completing one of the following:": "Patvirtinkite šį seansą atlikdami vieną iš šių veiksmų:", + "Scan this unique code": "Nuskaitykite šį unikalų kodą", + "Compare unique emoji": "Palyginkite unikalius jaustukus", + "Compare a unique set of emoji if you don't have a camera on either device": "Palyginkite unikalų jaustukų rinkinį, jei neturite fotoaparato nei viename įrenginyje", + "Confirm the emoji below are displayed on both sessions, in the same order:": "Patvirtinkite, kad žemiau esantys jaustukai yra rodomi abiejuose seansuose, ta pačia tvarka:", + "Waiting for %(displayName)s to verify…": "Laukiama kol %(displayName)s patvirtins…", + "Cancelling…": "Atšaukiama…", + "They match": "Jie sutampa", + "They don't match": "Jie nesutampa", + "Dog": "Šuo", + "Cat": "Katė", + "Lion": "Liūtas", + "Horse": "Arklys", + "Unicorn": "Vienaragis", + "Pig": "Kiaulė", + "Elephant": "Dramblys", + "Rabbit": "Triušis", + "Panda": "Panda", + "Rooster": "Gaidys", + "Penguin": "Pingvinas", + "Turtle": "Vėžlys", + "Fish": "Žuvis", + "Octopus": "Aštunkojis", + "Butterfly": "Drugelis", + "Flower": "Gėlė", + "Tree": "Medis", + "Cactus": "Kaktusas", + "Mushroom": "Grybas", + "Globe": "Gaublys", + "Moon": "Mėnulis", + "Cloud": "Debesis", + "Fire": "Ugnis", + "Banana": "Bananas", + "Apple": "Obuolys", + "Strawberry": "Braškė", + "Corn": "Kukurūzas", + "Pizza": "Pica", + "Cake": "Tortas", + "Heart": "Širdis", + "Smiley": "Šypsenėlė", + "Robot": "Robotas", + "Hat": "Skrybėlė", + "Glasses": "Akiniai", + "Spanner": "Veržliaraktis", + "Santa": "Santa", + "Thumbs up": "Liuks", + "Umbrella": "Skėtis", + "Hourglass": "Smėlio laikrodis", + "Clock": "Laikrodis", + "Gift": "Dovana", + "Light bulb": "Lemputė", + "Book": "Knyga", + "Pencil": "Pieštukas", + "Paperclip": "Sąvaržėlė", + "Scissors": "Žirklės", + "Lock": "Spyna", + "Key": "Raktas", + "Hammer": "Plaktukas", + "Telephone": "Telefonas", + "Flag": "Vėliava", + "Train": "Traukinys", + "Bicycle": "Dviratis", + "Aeroplane": "Lėktuvas", + "Rocket": "Raketa", + "Trophy": "Trofėjus", + "Ball": "Kamuolys", + "Guitar": "Gitara", + "Trumpet": "Trimitas", + "Bell": "Varpas", + "Anchor": "Inkaras", + "Headphones": "Ausinės", + "Folder": "Aplankas", + "Pin": "Smeigtukas", + "Other users may not trust it": "Kiti vartotojai gali nepasitikėti", + "Upgrade": "Atnaujinti", + "From %(deviceName)s (%(deviceId)s)": "Iš %(deviceName)s (%(deviceId)s)", + "Decline (%(counter)s)": "Atsisakyti (%(counter)s)", + "Accept to continue:": "Sutikite su , kad tęstumėte:", + "Upload": "Įkelti", + "Your homeserver does not support cross-signing.": "Jūsų serveris nepalaiko kryžminio pasirašymo.", + "Cross-signing and secret storage are enabled.": "Kryžminis pasirašymas ir slapta saugykla yra įjungta.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Jūsų paskyra slaptoje saugykloje turi kryžminio pasirašymo tapatybę, bet šis seansas dar ja nepasitiki.", + "Cross-signing and secret storage are not yet set up.": "Kryžminis pasirašymas ir slapta saugykla dar nėra nustatyti.", + "Reset cross-signing and secret storage": "Atstatyti kryžminį pasirašymą ir slaptą saugyklą", + "Bootstrap cross-signing and secret storage": "Prikabinti kryžminį pasirašymą ir slaptą saugyklą", + "Cross-signing public keys:": "Kryžminio pasirašymo vieši raktai:", + "Cross-signing private keys:": "Kryžminio pasirašymo privatūs raktai:", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individualiai patikrinkite kiekvieną vartotojo naudojamą seansą, kad pažymėtumėte jį kaip patikimą, nepasitikint kryžminiu pasirašymu patvirtintais įrenginiais.", + "Enable": "Įjungti", + "Backup has a valid signature from verified session ": "Atsarginė kopija turi galiojantį parašą iš patikrinto seanso ", + "Backup has a valid signature from unverified session ": "Atsarginė kopija turi galiojantį parašą iš nepatikrinto seanso ", + "Backup has an invalid signature from verified session ": "Atsarginė kopija turi negaliojantį parašą iš patikrinto seanso ", + "Backup has an invalid signature from unverified session ": "Atsarginė kopija turi negaliojantį parašą iš nepatikrinto seanso ", + "Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Atsarginė rakto kopija saugoma slaptoje saugykloje, bet ši funkcija nėra įjungta šiame seanse. Įjunkite kryžminį pasirašymą Laboratorijose, kad galėtumėte keisti atsarginės rakto kopijos būseną.", + "Enable desktop notifications for this session": "Įjungti darbalaukio pranešimus šiam seansui", + "Enable audible notifications for this session": "Įjungti garsinius pranešimus šiam seansui", + "wait and try again later": "palaukite ir bandykite vėliau dar kartą", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jei jūs nenorite naudoti radimui ir tam, kad būtumėte randamas esamų, jums žinomų kontaktų, žemiau įveskite kitą tapatybės serverį.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Tapatybės serverio naudojimas yra pasirinktinis. Jei jūs pasirinksite jo nenaudoti, jūs nebūsite randamas kitų vartotojų ir neturėsite galimybės pakviesti kitų nurodydamas el. paštą ar telefoną.", + "Do not use an identity server": "Nenaudoti tapatybės serverio", + "Cross-signing": "Kryžminis pasirašymas", + "Error changing power level requirement": "Klaida keičiant galios lygio reikalavimą", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Keičiant kambario galios lygio reikalavimus įvyko klaida. Įsitikinkite, kad turite tam leidimą ir bandykite dar kartą.", + "Error changing power level": "Klaida keičiant galios lygį", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Keičiant vartotojo galios lygį įvyko klaida. Įsitikinkite, kad turite tam leidimą ir bandykite dar kartą.", + "Default role": "Numatytoji rolė", + "This user has not verified all of their sessions.": "Šis vartotojas nepatvirtino visų savo seansų.", + "You have not verified this user.": "Jūs nepatvirtinote šio vartotojo.", + "You have verified this user. This user has verified all of their sessions.": "Jūs patvirtinote šį vartotoją. Šis vartotojas patvirtino visus savo seansus.", + "Everyone in this room is verified": "Visi šiame kambaryje yra patvirtinti", + "Encrypted by a deleted session": "Užšifruota ištrintos sesijos", + "Use an identity server in Settings to receive invites directly in Riot.": "Nustatymuose naudokite tapatybės serverį, kad gautumėte pakvietimus tiesiai į Riot.", + "%(count)s verified sessions|one": "1 patvirtintas seansas", + "If you can't scan the code above, verify by comparing unique emoji.": "Jei nuskaityti aukščiau esančio kodo negalite, patvirtinkite palygindami unikalius jaustukus.", + "You've successfully verified your device!": "Jūs sėkmingai patvirtinote savo įrenginį!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Jūs sėkmingai patvirtinote %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "Jūs sėkmingai patvirtinote %(displayName)s!", + "Verified": "Patvirtinta", + "Got it": "Supratau", + "Start verification again from the notification.": "Pradėkite patvirtinimą iš naujo pranešime.", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Naudokite numatytajį (%(defaultIdentityServerName)s) arba tvarkykite Nustatymuose.", + "Use an identity server to invite by email. Manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tvarkykite Nustatymuose.", + "Destroy cross-signing keys?": "Sunaikinti kryžminio pasirašymo raktus?", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Kryžminio pasirašymo raktų ištrinimas yra neatšaukiamas. Visi, kurie buvo jais patvirtinti, matys saugumo įspėjimus. Jūs greičiausiai nenorite to daryti, nebent praradote visus įrenginius, iš kurių galite patvirtinti kryžminiu pasirašymu.", + "Clear cross-signing keys": "Valyti kryžminio pasirašymo raktus", + "To verify that this session can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Tam, kad patvirtintumėte šio seanso patikimumą, patikrinkite ar raktas, kurį matote Vartotojo Nustatymuose tame įrenginyje, sutampa su raktu esančiu žemiau:", + "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.": "Patvirtinkite šį įrenginį, kad pažymėtumėte jį kaip patikimą. Pasitikėjimas šiuo įrenginiu suteikia jums ir kitiems vartotojams papildomos ramybės, kai naudojate visapusiškai užšifruotas žinutes.", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Šio įrenginio patvirtinimas pažymės jį kaip patikimą ir vartotojai, kurie patvirtino su jumis, pasitikės šiuo įrenginiu.", + "a new cross-signing key signature": "naujas kryžminio pasirašymo rakto parašas", + "a device cross-signing signature": "įrenginio kryžminio pasirašymo parašas", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Pasiekite savo saugių žinučių istoriją ir kryžminio pasirašymo tapatybę, naudojamą kitų seansų patvirtinimui, įvesdami savo atgavimo raktą.", + "Session verified": "Seansas patvirtintas", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Neįmanoma prisijungti prie serverio per HTTP, kai naršyklės juostoje yra HTTPS URL. Naudokite HTTPS arba įjunkite nesaugias rašmenas.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Jūsų naujas seansas dabar yra patvirtintas. Jis turi prieigą prie jūsų šifruotų žinučių ir kiti vartotojai matys jį kaip patikimą.", + "Your new session is now verified. Other users will see it as trusted.": "Jūsų naujas seansas dabar yra patvirtintas. Kiti vartotojai matys jį kaip patikimą.", + "NOT verified": "Nepatvirtinta", + "verified": "patvirtinta", + "If you don't want to set this up now, you can later in Settings.": "Jei jūs dabar nenorite to nustatyti, galite padaryti tai vėliau Nustatymuose.", + "Done": "Atlikta", + "No media permissions": "Nėra medijos leidimų", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "Seansas, kurį jūs bandote patvirtinti, nepalaiko QR kodo nuskaitymo arba jaustukų patvirtinimo, kuriuos palaiko Riot. Bandykite su kitu klientu.", + "Almost there! Is your other session showing the same shield?": "Beveik atlikta! Ar jūsų kitas seansas rodo tokį patį skydą?", + "Almost there! Is %(displayName)s showing the same shield?": "Beveik atlikta! Ar %(displayName)s rodo tokį patį skydą?", + "No": "Ne", + "Yes": "Taip", + "Interactively verify by Emoji": "Patvirtinti interaktyviai, naudojant jaustukus", + "The message you are trying to send is too large.": "Žinutė, kurią jūs bandote siųsti, yra per didelė.", + "Use the improved room list (in development - refresh to apply changes)": "Naudoti patobulintą kambarių sąrašą (tobulinama - atnaujinkite, kad pritaikytumėte pakeitimus)", + "Show a placeholder for removed messages": "Rodyti pašalintų žinučių žymeklį", + "Show join/leave messages (invites/kicks/bans unaffected)": "Rodyti prisijungimo/išėjimo žinutes (pakvietimai/išmetimai/draudimai nepaveikti)", + "Show avatar changes": "Rodyti pseudoportretų pakeitimus", + "Show avatars in user and room mentions": "Rodyti pseudoportretus vartotojo ir kambario paminėjimuose", + "Send typing notifications": "Siųsti spausdinimo pranešimus", + "Automatically replace plain text Emoji": "Automatiškai pakeisti paprasto teksto jaustukus", + "Mirror local video feed": "Atkartoti lokalų video tiekimą", + "Allow Peer-to-Peer for 1:1 calls": "Leisti \"Peer-to-Peer\" 1:1 skambučiams", + "Prompt before sending invites to potentially invalid matrix IDs": "Klausti prieš siunčiant pakvietimus galimai netinkamiems matrix ID", + "Show rooms with unread notifications first": "Pirmiausia rodyti kambarius su neskaitytais pranešimais", + "Show shortcuts to recently viewed rooms above the room list": "Rodyti neseniai peržiūrėtų kambarių nuorodas virš kambarių sąrašo", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Leisti atsarginį skambučių pagalbos serverį turn.matrix.org, kai jūsų serveris to neteikia (jūsų IP adresas bus bendrintas pokalbio metu)", + "Show previews/thumbnails for images": "Rodyti vaizdų peržiūras/miniatiūras", + "IRC display name width": "IRC rodomo vardo plotis", + "Encrypted messages in one-to-one chats": "Šifruotos žinutės privačiuose pokalbiuose", + "When rooms are upgraded": "Kai atnaujinami kambariai", + "My Ban List": "Mano Draudimų Sąrašas", + "This is your list of users/servers you have blocked - don't leave the room!": "Tai jūsų užblokuotų vartotojų/serverių sąrašas - nepalikite kambario!", + "Verify this user by confirming the following emoji appear on their screen.": "Patvirtinkite šį vartotoją, įsitikindami, kad šie jaustukai rodomi jo ekrane.", + "⚠ These settings are meant for advanced users.": "⚠ Šie nustatymai yra skirti pažengusiems vartotojams.", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Jūsų asmeniniame draudimų sąraše yra visi vartotojai/serveriai, iš kurių jūs asmeniškai nenorite matyti pranešimų. Po pirmojo jūsų vartotojo/serverio ignoravimo, jūsų kambarių sąraše pasirodys naujas kambarys pavadinimu 'Mano Draudimų Sąrašas' - likite šiame kambaryje, kad draudimų sąrašas veiktų.", + "Room ID or alias of ban list": "Kambario ID arba draudimų sąrašo slapyvardis", + "Room list": "Kambarių sąrašas", + "Composer": "Rašymas", + "Autocomplete delay (ms)": "Automatinio užbaigimo vėlinimas (ms)", + "Read Marker lifetime (ms)": "Skaitymo žymeklio veikimo laikas (ms)", + "Read Marker off-screen lifetime (ms)": "Skaitymo žymeklio ne ekraninis veikimo laikas (ms)", + "You can use /help to list available commands. Did you mean to send this as a message?": "Jūs galite naudoti /help, kad pamatytumėte galimų komandų sąrašą. Ar norėjote siųsti tai kaip žinutę?", + "Room avatar": "Kambario pseudoportretas", + "Verify by comparing unique emoji.": "Patvirtinkite palygindami unikalius jaustukus.", + "Verify by emoji": "Patvirtinti naudojant jaustukus", + "Compare emoji": "Palyginkite jaustukus", + "Show image": "Rodyti vaizdą", + "Your avatar URL": "Jūsų pseudoportreto URL", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Jei yra papildomo konteksto, kuris padėtų analizuojant šią problemą, tokio kaip ką jūs darėte tuo metu, kambarių ID, vartotojų ID ir t.t., įtraukite tuos dalykus čia.", + "Create a new room with the same name, description and avatar": "Sukurti naują kambarį su tuo pačiu pavadinimu, aprašymu ir pseudoportretu", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Kambario atnaujinimas yra sudėtingas veiksmas ir paprastai rekomenduojamas, kai kambarys nestabilus dėl klaidų, trūkstamų funkcijų ar saugos spragų.", + "To help us prevent this in future, please send us logs.": "Norėdami padėti mums išvengti to ateityje, atsiųskite mums žurnalus.", + "Failed to upload image": "Nepavyko įkelti vaizdo", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Pakeitimai atlikti jūsų bendruomenės pavadinimui ir pseudoportretui iki 30 minučių gali būti nematomi kitų vartotojų.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Norėdami nustatyti filtrą, vilkite bendruomenės pseudoportretą į filtrų skydelį, esantį kairėje ekrano pusėje. Jūs bet kada galite paspausti ant pseudoportreto filtrų skydelyje, kad pamatytumėte tik su ta bendruomene susijusius kambarius ir žmones.", + "Upload an avatar:": "Įkelti pseudoportretą:", + "Emoji": "Jaustukai", + "Emoji Autocomplete": "Jaustukų automatinis užbaigimas", + "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.": "Eksportuotas failas leis visiems, kurie gali jį perskaityti, iššifruoti visas užšifruotas žinutes, kurias jūs galite matyti, todėl turėtumėte būti atsargūs, kad jis būtų saugus. Tam padėti, jūs turėtumėte žemiau įvesti slaptafrazę, kuri bus naudojama eksportuotų duomenų užšifravimui. Duomenis bus galima importuoti tik naudojant tą pačią slaptafrazę.", + "Jump to start/end of the composer": "Peršokti į rašymo pradžią/pabaigą", + "Navigate composer history": "Naršyti rašymo istoriją", + "Navigate up/down in the room list": "Naršyti aukštyn/žemyn kambarių saraše", + "Select room from the room list": "Pasirinkite kambarį iš kambarių sąrašo", + "Collapse room list section": "Sutraukti kambarių sąrašo skyrių", + "Expand room list section": "Išplėsti kambarių sąrašo skyrių", + "Clear room list filter field": "Išvalyti kambarių sąrašo filtro lauką", + "Community IDs cannot be empty.": "Bendruomenių ID negali būti tušti.", + "Failed to reject invitation": "Nepavyko atmesti pakvietimo", + "Failed to leave room": "Nepavyko išeiti iš kambario", + "Can't leave Server Notices room": "Negalima išeiti iš Serverio Pranešimų kambario", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "Šis kambarys yra naudojamas svarbioms žinutėms iš serverio, todėl jūs negalite jo palikti.", + "Terms and Conditions": "Taisyklės ir Sąlygos", + "Self-verification request": "Savipatvirtinimo užklausa", + "Logout": "Atsijungti", + "Reject & Ignore user": "Atmesti ir ignoruoti vartotoją", + "Reject invitation": "Atmesti pakvietimą", + "Unable to reject invite": "Nepavyko atmesti pakvietimo", + "Whether you're using Riot as an installed Progressive Web App": "Nesvarbu, ar naudojate Riot kaip įdiegtą progresyviąją žiniatinklio programą", + "Your user agent": "Jūsų vartotojo agentas", + "The information being sent to us to help make Riot better includes:": "Informacija, siunčiama mums siekiant pagerinti Riot, yra:", + "Invite only": "Tik pakviestiems", + "You can only join it with a working invite.": "Jūs galite prisijungti tik su veikiančiu pakvietimu.", + "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šiame kambaryje yra nežinomų seansų: jei jūs tęsite jų nepatvirtinę, bus galimybė kažkam slapta pasiklausyti jūsų skambučio.", + "If you cancel now, you won't complete your operation.": "Jei atšauksite dabar, jūs neužbaigsite savo operacijos.", + "Cancel entering passphrase?": "Atšaukti slaptafrazės įvedimą?", + "Room name or address": "Kambario pavadinimas arba adresas", + "%(name)s is requesting verification": "%(name)s prašo patvirtinimo", + "Sign In or Create Account": "Prisijungti arba Sukurti Paskyrą", + "Use your account or create a new one to continue.": "Norėdami tęsti naudokite savo paskyrą arba sukurkite naują.", + "Create Account": "Sukurti Paskyrą", + "Custom (%(level)s)": "Pasirinktinis (%(level)s)", + "Ask this user to verify their session, or manually verify it below.": "Paprašykite šio vartotojo patvirtinti savo seansą arba patvirtinkite jį rankiniu būdu žemiau.", + "Encryption upgrade available": "Galimas šifravimo atnaujinimas", + "Verify this user by confirming the following number appears on their screen.": "Patvirtinkite šį vartotoją įsitikindami, kad jo ekrane rodomas toliau esantis skaičius.", + "Backup has a signature from unknown user with ID %(deviceId)s": "Atsarginė kopija turi nežinomo vartotojo parašą su %(deviceId)s ID", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Tvarkykite savo seansų pavadinimus ir iš jų atsijunkite žemiau, arba patvirtinkite juos savo Vartotojo Profilyje.", + "Please enter verification code sent via text.": "Įveskite patvirtinimo kodą išsiųstą teksto žinute.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Teksto žinutė buvo išsiųsta numeriu +%(msisdn)s. Įveskite joje esantį patvirtinimo kodą.", + "Low priority": "Žemo prioriteto", + "New published address (e.g. #alias:server)": "Naujas paskelbtas adresas (pvz.: #pavadinimas:server)", + "Waiting for %(displayName)s to accept…": "Laukiama kol %(displayName)s sutiks…", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Jūsų žinutės yra apsaugotos ir tik jūs ir gavėjas turite unikalius raktus joms atrakinti.", + "This client does not support end-to-end encryption.": "Šis klientas nepalaiko visapusio šifravimo.", + "Start verification again from their profile.": "Pradėkite patvirtinimą iš naujo jų profilyje.", + "The encryption used by this room isn't supported.": "Šiame kambaryje naudojamas šifravimas nėra palaikomas.", + "You sent a verification request": "Jūs išsiuntėte patvirtinimo užklausą", + "Widgets do not use message encryption.": "Valdikliai nenaudoja žinučių šifravimo.", + "Continue With Encryption Disabled": "Tęsti išjungus šifravimą", + "Waiting for partner to accept...": "Laukiama kol partneris sutiks...", + "Waiting for %(userId)s to confirm...": "Laukiama kol %(userId)s patvirtins...", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Patvirtinkite šį vartotoją, kad pažymėtumėte jį kaip patikimą. Pažymint vartotojus kaip patikimus suteikia papildomos ramybės naudojant visapusiškai užšifruotas žinutes.", + "Waiting for partner to confirm...": "Laukiama kol partneris patvirtins...", + "Start a conversation with someone using their name, username (like ) or email address.": "Pradėkite pokalbį su kuo nors naudodami jų vardą, vartotojo vardą (kaip ) arba el. pašto adresą.", + "Invite someone using their name, username (like ), email address or share this room.": "Pakvieskite ką nors naudodami jų vardą, vartotojo vardą (kaip ), el. pašto adresą arba bendrinkite šį kambarį.", + "Sign out and remove encryption keys?": "Atsijungti ir pašalinti šifravimo raktus?", + "This will be your account name on the homeserver, or you can pick a different server.": "Tai bus jūsų paskyros pavadinimas serveryje, arba jūs galite pasirinkti kitą serverį.", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Valdiklis, esantis %(widgetUrl)s nori patvirtinti jūsų tapatybę. Jei tai leisite, valdiklis galės patvirtinti jūsų vartotojo ID, bet neatliks veiksmų kaip jūs.", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Ištrinti kambario adresą %(alias)s ir pašalinti %(name)s iš katalogo?", + "delete the address.": "ištrinti adresą.", + "Preview": "Peržiūrėti", + "View": "Žiūrėti", + "Confirm encryption setup": "Patvirtinti šifravimo sąranką", + "Click the button below to confirm setting up encryption.": "Paspauskite mygtuką žemiau, kad patvirtintumėte šifravimo nustatymą.", + "Restore your key backup to upgrade your encryption": "Atstatykite savo raktų atsarginę kopiją, kad atnaujintumėte šifravimą", + "Upgrade your encryption": "Atnaujinkite savo šifravimą", + "Failed to set topic": "Nepavyko nustatyti temos" } diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 59182a0249..8554e3d86b 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1342,5 +1342,7 @@ "Enter a recovery passphrase...": "Skriv inn en gjenopprettingspassfrase …", "Please enter your recovery passphrase a second time to confirm.": "Skriv inn gjenopprettingspassfrasen din en andre gang for å bekrefte.", "Repeat your recovery passphrase...": "Gjenta gjenopprettingspassfrasen din …", - "Other users may not trust it": "Andre brukere kan kanskje mistro den" + "Other users may not trust it": "Andre brukere kan kanskje mistro den", + " reacted with %(content)s": " reagerte med %(content)s", + "reacted with %(shortName)s": " reagerte med %(shortName)s" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 5dfb779ff1..b4bb5dc5bd 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -45,7 +45,7 @@ "Continue": "Doorgaan", "Could not connect to the integration server": "Verbinding met de integratieserver is mislukt", "Cancel": "Annuleren", - "Accept": "Aanvaarden", + "Accept": "Aannemen", "Active call (%(roomName)s)": "Actieve oproep (%(roomName)s)", "Add": "Toevoegen", "Add a topic": "Voeg een onderwerp toe", @@ -140,7 +140,7 @@ "Create Room": "Gesprek aanmaken", "Curve25519 identity key": "Curve25519-identiteitssleutel", "/ddg is not a command": "/ddg is geen opdracht", - "Deactivate Account": "Account deactiveren", + "Deactivate Account": "Account sluiten", "Decline": "Weigeren", "Decrypt %(text)s": "%(text)s ontsleutelen", "Decryption error": "Ontsleutelingsfout", @@ -283,7 +283,7 @@ "The phone number entered looks invalid": "Het ingevoerde telefoonnummer ziet er ongeldig uit", "This email address is already in use": "Dit e-mailadres is al in gebruik", "This email address was not found": "Dit e-mailadres is niet gevonden", - "The email address linked to your account must be entered.": "Het e-mailadres dat met uw account verbonden is moet ingevoerd worden.", + "The email address linked to your account must be entered.": "Het aan uw account gekoppelde e-mailadres dient ingevoerd worden.", "The remote side failed to pick up": "De andere kant heeft niet opgenomen", "This room has no local addresses": "Dit gesprek heeft geen lokale adressen", "This room is not recognised.": "Dit gesprek wordt niet herkend.", @@ -378,7 +378,7 @@ "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.": "Hiermee kunt u vanuit een andere Matrix-cliënt weggeschreven versleutelingssleutels inlezen, zodat u alle berichten die de andere cliënt kon ontcijferen ook hier kunt lezen.", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Het weggeschreven bestand is beveiligd met een wachtwoord. Voer dat wachtwoord hier in om het bestand te ontsleutelen.", "You must join the room to see its files": "Slechts na toetreding tot het gesprek zult u de bestanden kunnen zien", - "Reject all %(invitedRooms)s invites": "Alle %(invitedRooms)s-uitnodigingen weigeren", + "Reject all %(invitedRooms)s invites": "Alle %(invitedRooms)s de uitnodigingen weigeren", "Failed to invite": "Uitnodigen is mislukt", "Failed to invite the following users to the %(roomName)s room:": "Kon de volgende gebruikers niet uitnodigen voor gesprek %(roomName)s:", "Confirm Removal": "Verwijdering bevestigen", @@ -584,11 +584,11 @@ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s zijn weggegaan en weer toegetreden", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s is %(count)s keer weggegaan en weer toegetreden", "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s is weggegaan en weer toegetreden", - "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s hebben hun uitnodigingen %(count)s keer afgewezen", - "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s hebben hun uitnodigingen afgewezen", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s hebben hun uitnodigingen %(count)s maal afgeslagen", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s hebben hun uitnodigingen afgeslagen", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s heeft de uitnodiging %(count)s maal geweigerd", "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s heeft de uitnodiging geweigerd", - "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "De uitnodigingen van %(severalUsers)s zijn %(count)s keer ingetrokken", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s hebben hun uitnodigingen %(count)s maal ingetrokken", "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "De uitnodigingen van %(severalUsers)s zijn ingetrokken", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "De uitnodiging van %(oneUser)s is %(count)s keer ingetrokken", "%(oneUser)shad their invitation withdrawn %(count)s times|one": "De uitnodiging van %(oneUser)s is ingetrokken", @@ -648,7 +648,7 @@ "Failed to remove a user from the summary of %(groupId)s": "Verwijderen van gebruiker uit het overzicht van %(groupId)s is mislukt", "The user '%(displayName)s' could not be removed from the summary.": "De gebruiker ‘%(displayName)s’ kon niet uit het overzicht verwijderd worden.", "Failed to update community": "Bijwerken van gemeenschap is mislukt", - "Unable to accept invite": "Kan de uitnodiging niet aanvaarden", + "Unable to accept invite": "Kan de uitnodiging niet aannemen", "Unable to reject invite": "Kan de uitnodiging niet weigeren", "Leave Community": "Gemeenschap verlaten", "Leave %(groupName)s?": "%(groupName)s verlaten?", @@ -741,7 +741,7 @@ "Send Custom Event": "Aangepaste gebeurtenis versturen", "Advanced notification settings": "Geavanceerde meldingsinstellingen", "delete the alias.": "verwijder de bijnaam.", - "To return to your account in future you need to set a password": "Tenzij u een wachtwoord instelt zult u uw account niet meer kunnen benaderen", + "To return to your account in future you need to set a password": "Om uw account te kunnen blijven gebruiken dient u een wachtwoord in te stellen", "Forget": "Vergeten", "You cannot delete this image. (%(code)s)": "U kunt deze afbeelding niet verwijderen. (%(code)s)", "Cancel Sending": "Versturen annuleren", @@ -830,7 +830,7 @@ "Riot does not know how to join a room on this network": "Riot weet niet hoe het moet deelnemen aan een gesprek op dit netwerk", "Mentions only": "Alleen vermeldingen", "Wednesday": "Woensdag", - "You can now return to your account after signing out, and sign in on other devices.": "U kunt nu terugkeren naar uw account nadat u zich heeft afgemeld, en u aanmelden op andere apparaten.", + "You can now return to your account after signing out, and sign in on other devices.": "Na afmelding kunt u terugkeren tot uw account, en u op andere apparaten aanmelden.", "Enable email notifications": "E-mailmeldingen inschakelen", "Event Type": "Gebeurtenistype", "Download this file": "Dit bestand downloaden", @@ -863,10 +863,10 @@ "Yes, I want to help!": "Ja, ik wil helpen!", "Popout widget": "Widget in nieuw venster openen", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kan de gebeurtenis waarop gereageerd was niet laden. Wellicht bestaat die niet, of heeft u geen toestemming die te bekijken.", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal zich met dezelfde gebruikers-ID kunnen registreren. Hierdoor zal uw account alle gesprekken waaraan deze deelneemt verlaten, en worden de accountgegevens verwijderd van de identiteitsserver. Deze actie is onomkeerbaar.", - "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Het deactiveren van uw account zal er standaard niet voor zorgen dat de berichten die u heeft verstuurd vergeten worden. Als u wilt dat wij de berichten vergeten, vinkt u het vakje hieronder aan.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal zich met dezelfde gebruikers-ID kunnen registreren. Hierdoor zal uw account alle gesprekken waaraan ze deelneemt verlaten, en worden de accountgegevens verwijderd van de identiteitsserver. Deze stap is onomkeerbaar.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Het sluiten van uw account maakt op zich niet dat wij de door u verstuurde berichten vergeten. Als u wilt dat wij uw berichten vergeten, vink dan het vakje hieronder aan.", "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "De zichtbaarheid van berichten in Matrix is zoals bij e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.", - "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is (Let op: dit zal er voor zorgen dat toekomstige gebruikers een onvolledig beeld krijgen van gesprekken)", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Vergeet bij het sluiten van mijn account alle door mij verstuurde berichten (Let op: hierdoor zullen gebruikers een onvolledig beeld krijgen van gesprekken)", "To continue, please enter your password:": "Voer uw wachtwoord in om verder te gaan:", "Clear Storage and Sign Out": "Opslag wissen en afmelden", "Send Logs": "Logboek versturen", @@ -877,7 +877,7 @@ "Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten", "This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de thuisserver, dus u kunt het niet verlaten.", "Terms and Conditions": "Gebruiksvoorwaarden", - "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Om de %(homeserverDomain)s-thuisserver te blijven gebruiken, zult u de gebruiksvoorwaarden moeten lezen en aanvaarden.", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Om de %(homeserverDomain)s-thuisserver te blijven gebruiken, zult u de gebruiksvoorwaarden moeten bestuderen en aanvaarden.", "Review terms and conditions": "Gebruiksvoorwaarden lezen", "Call in Progress": "Lopend gesprek", "A call is currently being placed!": "Er wordt al een oproep gemaakt!", @@ -989,7 +989,7 @@ "Render simple counters in room header": "Eenvoudige tellers bovenaan het gesprek tonen", "Enable Emoji suggestions while typing": "Emoticons voorstellen tijdens het typen", "Show a placeholder for removed messages": "Vulling tonen voor verwijderde berichten", - "Show join/leave messages (invites/kicks/bans unaffected)": "Berichten over deelnamen en verlatingen tonen (dit heeft geen effect op uitnodigingen, berispingen of verbanningen)", + "Show join/leave messages (invites/kicks/bans unaffected)": "Berichten over toe- en uittredingen tonen (dit heeft geen effect op uitnodigingen, berispingen of verbanningen)", "Show avatar changes": "Veranderingen van avatar tonen", "Show display name changes": "Veranderingen van weergavenamen tonen", "Show read receipts sent by other users": "Door andere gebruikers verstuurde leesbevestigingen tonen", @@ -1107,7 +1107,7 @@ "Language and region": "Taal en regio", "Theme": "Thema", "Account management": "Accountbeheer", - "Deactivating your account is a permanent action - be careful!": "Het deactiveren van uw account kan niet ongedaan gemaakt worden - wees voorzichtig!", + "Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account kan niet ongedaan gemaakt worden!", "General": "Algemeen", "Legal": "Wettelijk", "Credits": "Met dank aan", @@ -1122,7 +1122,7 @@ "Timeline": "Tijdslijn", "Room list": "Gesprekslijst", "Autocomplete delay (ms)": "Vertraging voor automatisch aanvullen (ms)", - "Accept all %(invitedRooms)s invites": "Alle %(invitedRooms)s-uitnodigingen aanvaarden", + "Accept all %(invitedRooms)s invites": "Alle %(invitedRooms)s de uitnodigingen aannemen", "Key backup": "Sleutelback-up", "Security & Privacy": "Veiligheid & privacy", "Missing media permissions, click the button below to request.": "Mediatoestemmingen ontbreken, klik op de knop hieronder om deze aan te vragen.", @@ -1210,7 +1210,7 @@ "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifieer deze gebruiker om hem/haar als vertrouwd te markeren. Gebruikers vertrouwen geeft u extra gemoedsrust bij het gebruik van eind-tot-eind-versleutelde berichten.", "Waiting for partner to confirm...": "Wachten op bevestiging van partner…", "Incoming Verification Request": "Inkomend verificatieverzoek", - "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "U heeft voorheen Riot op %(host)s gebruikt met lui laden van leden ingeschakeld. In deze versie is lui laden uitgeschakeld. Omdat de lokale cache niet compatibel is tussen deze twee instellingen, moet Riot uw account opnieuw synchroniseren.", + "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "U heeft voorheen Riot op %(host)s gebruikt met lui laden van leden ingeschakeld. In deze versie is lui laden uitgeschakeld. De lokale cache is niet compatibel tussen deze twee instellingen, zodat Riot uw account moet hersynchroniseren.", "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Indien de andere versie van Riot nog open staat in een ander tabblad kunt u dat beter sluiten, want het geeft problemen als Riot op dezelfde host gelijktijdig met lui laden ingeschakeld en uitgeschakeld draait.", "Incompatible local cache": "Incompatibele lokale cache", "Clear cache and resync": "Cache wissen en hersynchroniseren", @@ -1261,18 +1261,18 @@ "Set a new status...": "Stel een nieuwe status in…", "Hide": "Verbergen", "This homeserver would like to make sure you are not a robot.": "Deze thuisserver wil graag weten of u geen robot bent.", - "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "U kunt de aangepaste serveropties gebruiken om u aan te melden bij andere Matrix-servers, door een andere thuisserver-URL op te geven. Dit biedt u de mogelijkheid om deze toepassing te gebruiken met een bestaande Matrix-account op een andere thuisserver.", - "Please review and accept all of the homeserver's policies": "Gelieve het beleid van de thuisserver te doornemen en aanvaarden", - "Please review and accept the policies of this homeserver:": "Gelieve het beleid van deze thuisserver te doornemen en aanvaarden:", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "Middels de aangepaste serveropties kunt u zich aanmelden bij andere Matrix-servers, door een andere thuisserver-URL op te geven. Zo kunt u deze toepassing met een bestaande Matrix-account op een andere thuisserver gebruiken.", + "Please review and accept all of the homeserver's policies": "Gelieve het beleid van de thuisserver door te nemen en te aanvaarden", + "Please review and accept the policies of this homeserver:": "Gelieve het beleid van deze thuisserver door te nemen en te aanvaarden:", "Your Modular server": "Uw Modular-server", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Voer de locatie van uw Modular-thuisserver in. Deze kan uw eigen domeinnaam gebruiken, of een subdomein van modular.im zijn.", "Server Name": "Servernaam", "The username field must not be blank.": "Het gebruikersnaamveld mag niet leeg zijn.", "Username": "Gebruikersnaam", - "Not sure of your password? Set a new one": "Onzeker over uw wachtwoord? Stel er een nieuw in", + "Not sure of your password? Set a new one": "Onzeker over uw wachtwoord? Stel een nieuw in", "Sign in to your Matrix account on %(serverName)s": "Aanmelden met uw Matrix-account op %(serverName)s", "Change": "Wijzigen", - "Create your Matrix account on %(serverName)s": "Maak uw Matrix-account aan op %(serverName)s", + "Create your Matrix account on %(serverName)s": "Maak uw Matrix-account op %(serverName)s aan", "Email (optional)": "E-mailadres (optioneel)", "Phone (optional)": "Telefoonnummer (optioneel)", "Confirm": "Bevestigen", @@ -1444,7 +1444,7 @@ "Edit message": "Bericht bewerken", "View Servers in Room": "Servers in gesprek bekijken", "Unable to validate homeserver/identity server": "Kan thuis-/identiteitsserver niet valideren", - "Sign in to your Matrix account on ": "Meld u aan met uw Matrix-account op ", + "Sign in to your Matrix account on ": "Meld u met uw Matrix-account op aan", "Use an email address to recover your account": "Gebruik een e-mailadres om uw account te herstellen", "Enter email address (required on this homeserver)": "Voer een e-mailadres in (vereist op deze thuisserver)", "Doesn't look like a valid email address": "Dit lijkt geen geldig e-mailadres", @@ -1457,7 +1457,7 @@ "Doesn't look like a valid phone number": "Dit lijkt geen geldig telefoonnummer", "Enter username": "Voer gebruikersnaam in", "Some characters not allowed": "Sommige tekens zijn niet toegestaan", - "Create your Matrix account on ": "Maak uw Matrix-account aan op ", + "Create your Matrix account on ": "Maak uw Matrix-account op aan", "Add room": "Gesprek toevoegen", "Your profile": "Uw profiel", "Your Matrix account on ": "Uw Matrix-account op ", @@ -1485,7 +1485,7 @@ "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt uw wachtwoord opnieuw instellen, maar sommige functies zullen pas beschikbaar komen wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een serverbeheerder.", "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt zich aanmelden, maar sommige functies zullen pas beschikbaar zijn wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een systeembeheerder.", "Log in to your new account.": "Meld u aan met uw nieuwe account.", - "You can now close this window or log in to your new account.": "U kunt dit venster nu sluiten, of u aanmelden met uw nieuwe account.", + "You can now close this window or log in to your new account.": "U kunt dit venster nu sluiten, of u met uw nieuwe account aanmelden.", "Registration Successful": "Registratie geslaagd", "Upload all": "Alles versturen", "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Uw nieuwe account (%(newAccountId)s) is geregistreerd, maar u bent reeds aangemeld met een andere account (%(loggedInUserId)s).", @@ -1509,7 +1509,7 @@ "Resend removal": "Verwijdering opnieuw versturen", "Failed to re-authenticate due to a homeserver problem": "Opnieuw aanmelden is mislukt wegens een probleem met de thuisserver", "Failed to re-authenticate": "Opnieuw aanmelden is mislukt", - "Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en opnieuw toegang te verkrijgen tot uw account.", + "Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en toegang tot uw account te herkrijgen.", "Forgotten your password?": "Wachtwoord vergeten?", "You're signed out": "U bent afgemeld", "Clear personal data": "Persoonlijke gegevens wissen", @@ -1521,9 +1521,9 @@ "Terms of Service": "Gebruiksvoorwaarden", "Service": "Dienst", "Summary": "Samenvatting", - "Sign in and regain access to your account.": "Meld u aan en verkrijg opnieuw toegang tot uw account.", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem contact op met de beheerder van uw thuisserver voor meer informatie.", - "This account has been deactivated.": "Deze account is gedeactiveerd.", + "Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem voor meer informatie contact op met de beheerder van uw thuisserver.", + "This account has been deactivated.": "Deze account is gesloten.", "Messages": "Berichten", "Actions": "Acties", "Displays list of commands with usages and descriptions": "Toont een lijst van beschikbare opdrachten, met hun gebruiken en beschrijvingen", @@ -1539,7 +1539,7 @@ "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "De verbinding met uw identiteitsserver verbreken zal ertoe leiden dat u niet door andere gebruikers gevonden zal kunnen worden, en dat u anderen niet via e-mail of telefoon zal kunnen uitnodigen.", "Integration Manager": "Integratiebeheerder", "Discovery": "Ontdekking", - "Deactivate account": "Account deactiveren", + "Deactivate account": "Account sluiten", "Always show the window menu bar": "De venstermenubalk altijd tonen", "Unable to revoke sharing for email address": "Kan delen voor dit e-mailadres niet intrekken", "Unable to share email address": "Kan e-mailadres niet delen", @@ -1611,11 +1611,11 @@ "Strikethrough": "Doorstreept", "Code block": "Codeblok", "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Er is een fout opgetreden (%(errcode)s) bij het valideren van uw uitnodiging. U kunt deze informatie doorgeven aan een gespreksbeheerder.", - "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Deze uitnodiging tot %(roomName)s was verstuurd naar %(email)s, wat niet aan uw account gekoppeld is", - "Link this email with your account in Settings to receive invites directly in Riot.": "Koppel dit e-mailadres aan uw account in de instellingen om uitnodigingen automatisch te ontvangen in Riot.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Deze uitnodiging tot %(roomName)s was verstuurd naar %(email)s, dat niet aan uw account gekoppeld is", + "Link this email with your account in Settings to receive invites directly in Riot.": "Koppel in de instellingen dit e-mailadres aan uw account om uitnodigingen direct in Riot te ontvangen.", "This invite to %(roomName)s was sent to %(email)s": "Deze uitnodiging tot %(roomName)s was verstuurd naar %(email)s", - "Use an identity server in Settings to receive invites directly in Riot.": "Gebruik een identiteitsserver in de instellingen om uitnodigingen automatisch te ontvangen in Riot.", - "Share this email in Settings to receive invites directly in Riot.": "Deel dit e-mailadres in de instellingen om uitnodigingen automatisch te ontvangen in Riot.", + "Use an identity server in Settings to receive invites directly in Riot.": "Gebruik in de instellingen een identiteitsserver om uitnodigingen direct in Riot te ontvangen.", + "Share this email in Settings to receive invites directly in Riot.": "Deel in de instellingen dit e-mailadres om uitnodigingen direct in Riot te ontvangen.", "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. Gebruik de standaardserver (%(defaultIdentityServerName)s) of beheer de server in de Instellingen.", "Use an identity server to invite by email. Manage in Settings.": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de Instellingen.", "Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.", @@ -1623,8 +1623,8 @@ "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw thuisserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw thuisserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.", "Send report": "Rapport versturen", "Report Content": "Inhoud melden", - "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Stel een e-mailadres in voor accountherstel. Gebruik optioneel een e-mailadres of telefoonnummer om vindbaar te zijn voor bestaande contacten.", - "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Stel een e-mailadres in voor accountherstel. Gebruik optioneel een e-mailadres om vindbaar te zijn voor bestaande contacten.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Stel een e-mailadres voor accountherstel in. Gebruik eventueel een e-mailadres of telefoonnummer om vindbaar te zijn voor bestaande contacten.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Stel een e-mailadres voor accountherstel in. Gebruik eventueel een e-mailadres om vindbaar te zijn voor bestaande contacten.", "Enter your custom homeserver URL What does this mean?": "Voer uw aangepaste thuisserver-URL in Wat betekent dit?", "Enter your custom identity server URL What does this mean?": "Voer uw aangepaste identiteitsserver-URL in Wat betekent dit?", "Explore": "Ontdekken", @@ -1864,7 +1864,7 @@ "If disabled, messages from encrypted rooms won't appear in search results.": "Dit moet aan staan om te kunnen zoeken in versleutelde gesprekken.", "Indexed rooms:": "Geïndexeerde gesprekken:", "Cross-signing and secret storage are enabled.": "Kruiselings ondertekenen en sleutelopslag zijn ingeschakeld.", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Uw account heeft een identiteit voor kruiselings ondertekenen in de sleutelopslag, maar deze is nog niet vertrouwd door de huidige sessie.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Uw account heeft een identiteit voor kruiselings ondertekenen in de sleutelopslag, maar die wordt nog niet vertrouwd door de huidige sessie.", "Cross-signing and secret storage are not yet set up.": "Kruiselings ondertekenen en sleutelopslag zijn nog niet ingesteld.", "Bootstrap cross-signing and secret storage": "Kruiselings ondertekenen en sleutelopslag instellen", "Reset cross-signing and secret storage": "Kruiselings ondertekenen en sleutelopslag opnieuw instellen", @@ -1898,7 +1898,7 @@ "Homeserver feature support:": "Functies ondersteund door thuisserver:", "exists": "bestaat", "Sign In or Create Account": "Meld u aan of maak een account aan", - "Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak er een nieuwe aan om verder te gaan.", + "Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak een nieuwe aan om verder te gaan.", "Create Account": "Account aanmaken", "Displays information about a user": "Geeft informatie weer over een gebruiker", "Order rooms by name": "Gesprekken sorteren op naam", @@ -1938,7 +1938,7 @@ "Send as message": "Versturen als bericht", "Failed to connect to integration manager": "Verbinding met integratiebeheerder is mislukt", "Waiting for %(displayName)s to accept…": "Wachten tot %(displayName)s aanvaardt…", - "Accepting…": "Aanvaarden…", + "Accepting…": "Toestaan…", "Start Verification": "Verificatie beginnen", "Messages in this room are end-to-end encrypted.": "De berichten in dit gesprek worden eind-tot-eind-versleuteld.", "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Uw berichten zijn beveiligd, en enkel de ontvanger en u hebben de unieke sleutels om ze te ontsleutelen.", @@ -1995,7 +1995,7 @@ "You cancelled": "U heeft geannuleerd", "%(name)s declined": "%(name)s heeft geweigerd", "%(name)s cancelled": "%(name)s heeft geannuleerd", - "Accepting …": "Aanvaarden…", + "Accepting …": "Toestaan…", "Declining …": "Weigeren…", "%(name)s wants to verify": "%(name)s wil verifiëren", "You sent a verification request": "U heeft een verificatieverzoek verstuurd", @@ -2061,14 +2061,14 @@ "You added a new session '%(displayName)s', which is requesting encryption keys.": "U heeft een nieuwe sessie ‘%(displayName)s’ toegevoegd, die om versleutelingssleutels vraagt.", "Your unverified session '%(displayName)s' is requesting encryption keys.": "Uw ongeverifieerde sessie ‘%(displayName)s’ vraagt om versleutelingssleutels.", "Loading session info...": "Sessie-info wordt geladen…", - "Your account is not secure": "Uw account is niet veilig", + "Your account is not secure": "Uw account is onveilig", "Your password": "Uw wachtwoord", "This session, or the other session": "Deze sessie, of de andere sessie", "The internet connection either session is using": "De internetverbinding gebruikt door een van de sessies", "We recommend you change your password and recovery key in Settings immediately": "We raden u aan onmiddellijk uw wachtwoord en herstelsleutel te wijzigen in de instellingen", "New session": "Nieuwe sessie", "Use this session to verify your new one, granting it access to encrypted messages:": "Gebruik deze sessie om uw nieuwe sessie te verifiëren, waardoor deze laatste toegang verkrijgt tot versleutelde berichten:", - "If you didn’t sign in to this session, your account may be compromised.": "Als u zich niet heeft aangemeld bij deze sessie, is uw account mogelijk gecompromitteerd.", + "If you didn’t sign in to this session, your account may be compromised.": "Als u zich niet heeft aangemeld bij deze sessie, is uw account wellicht geschonden.", "This wasn't me": "Dat was ik niet", "Automatically invite users": "Gebruikers automatisch uitnodigen", "Upgrade private room": "Privégesprek bijwerken", @@ -2076,7 +2076,7 @@ "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Het bijwerken van een gesprek is een gevorderde actie en wordt meestal aanbevolen wanneer een gesprek onstabiel is door fouten, ontbrekende functies of problemen met de beveiliging.", "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug.": "Dit heeft meestal enkel een invloed op de manier waarop het gesprek door de server verwerkt wordt. Als u problemen met uw Riot ondervindt, dien dan een foutmelding in.", "You'll upgrade this room from to .": "U werkt dit gesprek bij van naar .", - "This will allow you to return to your account after signing out, and sign in on other sessions.": "Dit biedt u de mogelijkheid om terug te keren naar uw account nadat u zich heeft afgemeld, en om u aan te melden bij andere sessies.", + "This will allow you to return to your account after signing out, and sign in on other sessions.": "Daardoor kunt u na afmelding terugkeren tot uw account, en u bij andere sessies aanmelden.", "You are currently blacklisting unverified sessions; to send messages to these sessions you must verify them.": "U blokkeert momenteel niet-geverifieerde sessies; om berichten te sturen naar deze sessies moet u ze verifiëren.", "We recommend you go through the verification process for each session to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We raden u aan om het verificatieproces voor elke sessie te doorlopen om te bevestigen dat ze aan hun rechtmatige eigenaar toebehoren, maar u kunt het bericht ook opnieuw versturen zonder verificatie indien u dit wenst.", "Room contains unknown sessions": "Gesprek bevat onbekende sessies", @@ -2122,8 +2122,8 @@ "Go Back": "Terugkeren", "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Door uw wachtwoord te wijzigen stelt u alle eind-tot-eind-versleutelingssleutels op al uw sessies opnieuw in, waardoor uw versleutelde gespreksgeschiedenis onleesbaar wordt. Stel sleutelback-up in of schrijf uw gesprekssleutels van een andere sessie weg vooraleer u een nieuw wachtwoord instelt.", "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "U bent afgemeld bij al uw sessies en zult geen pushberichten meer ontvangen. Meld u op elk apparaat opnieuw aan om meldingen opnieuw in te schakelen.", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Verkrijg opnieuw de toegang tot uw account en herstel de versleutelingssleutels die in deze sessie opgeslagen zijn. Hierzonder zult u niet al uw beveiligde berichten in al uw sessies kunnen lezen.", - "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.": "Let op: uw persoonlijke gegevens (versleutelingssleutels inbegrepen) worden nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u zich wilt aanmelden met een andere account.", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Herwin toegang tot uw account en herstel de tijdens deze sessie opgeslagen versleutelingssleutels, zonder welke sommige van uw beveiligde berichten in al uw sessies onleesbaar zijn.", + "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.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u zich wilt aanmelden met een andere account.", "Command Autocomplete": "Opdrachten automatisch aanvullen", "DuckDuckGo Results": "DuckDuckGo-resultaten", "Sender session information": "Sessie-informatie van afzender", @@ -2177,7 +2177,7 @@ "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bevestig uw identiteit met Eenmalige Aanmelding om dit telefoonnummer toe te voegen.", "Confirm adding phone number": "Bevestig toevoegen van het telefoonnummer", "Click the button below to confirm adding this phone number.": "Klik op de knop hieronder om het toevoegen van dit telefoonnummer te bevestigen.", - "Review Sessions": "Sessieverificatie", + "Review Sessions": "Sessies nazien", "If you cancel now, you won't complete your operation.": "Als u de operatie afbreekt kunt u haar niet voltooien.", "Review where you’re logged in": "Kijk na waar u aangemeld bent", "New login. Was this you?": "Nieuwe aanmelding - was u dat?", @@ -2200,5 +2200,14 @@ "Verify your other session using one of the options below.": "Verifieer uw andere sessie op een van onderstaande wijzen.", "Manually Verify by Text": "Handmatig middels een tekst", "Interactively verify by Emoji": "Interactief middels emojis", - "Support adding custom themes": "Sta maatwerkthema's toe" + "Support adding custom themes": "Sta maatwerkthema's toe", + "Opens chat with the given user": "Start een tweegesprek met die gebruiker", + "Sends a message to the given user": "Zendt die gebruiker een bericht", + "Font scaling": "Lettergrootte", + "Use the improved room list (in development - refresh to apply changes)": "Gebruik de verbeterde gesprekslijst (in ontwikkeling - ververs om veranderingen te zien)", + "Verify all your sessions to ensure your account & messages are safe": "Controleer al uw sessies om zeker te zijn dat uw account & berichten veilig zijn", + "Verify the new login accessing your account: %(name)s": "Verifieer de nieuwe aanmelding op uw account: %(name)s", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "Bevestig uw intentie deze account te sluiten door met Single Sign On uw identiteit te bewijzen.", + "Are you sure you want to deactivate your account? This is irreversible.": "Weet u zeker dat u uw account wil sluiten? Dit is onomkeerbaar.", + "Confirm account deactivation": "Bevestig accountsluiting" } diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index a3418a174d..16d2fd276d 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -351,7 +351,7 @@ "Share room": "Del rom", "Community Invites": "Fellesskapsinvitasjonar", "Invites": "Invitasjonar", - "Favourites": "Favorittar", + "Favourites": "Yndlingar", "Rooms": "Rom", "Low priority": "Låg prioritet", "System Alerts": "Systemvarsel", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 3591d96120..be9c6bfdd4 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -97,13 +97,13 @@ "Who can read history?": "Кто может читать историю?", "You do not have permission to post to this room": "Вы не можете писать в эту комнату", "You have no visible notifications": "Нет видимых уведомлений", - "%(targetName)s accepted an invitation.": "%(targetName)s принял приглашение.", - "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принял приглашение от %(displayName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s принимает приглашение.", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принимает приглашение от %(displayName)s.", "Active call": "Активный вызов", "%(senderName)s answered the call.": "%(senderName)s ответил(а) на звонок.", - "%(senderName)s banned %(targetName)s.": "%(senderName)s заблокировал(а) %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s забанил(а) %(targetName)s.", "Call Timeout": "Нет ответа", - "%(senderName)s changed their profile picture.": "%(senderName)s изменил(а) свой аватар.", + "%(senderName)s changed their profile picture.": "%(senderName)s изменяет свой аватар.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s изменил(а) уровни прав %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s изменил(а) название комнаты на %(roomName)s.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\".", @@ -223,8 +223,8 @@ "Reason": "Причина", "%(targetName)s rejected the invitation.": "%(targetName)s отклонил(а) приглашение.", "Reject invitation": "Отклонить приглашение", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удалил(а) свое отображаемое имя (%(oldDisplayName)s).", - "%(senderName)s removed their profile picture.": "%(senderName)s удалил(а) свой аватар.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удаляет своё отображаемое имя (%(oldDisplayName)s).", + "%(senderName)s removed their profile picture.": "%(senderName)s удаляет свой аватар.", "%(senderName)s requested a VoIP conference.": "%(senderName)s хочет начать конференц-звонок.", "Riot does not have permission to send you notifications - please check your browser settings": "У Riot нет разрешения на отправку уведомлений — проверьте настройки браузера", "Riot was not given permission to send notifications - please try again": "Riot не получил разрешение на отправку уведомлений, пожалуйста, попробуйте снова", @@ -302,8 +302,8 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Возможно, сервер недоступен, перегружен или случилась ошибка.", "Server unavailable, overloaded, or something else went wrong.": "Возможно, сервер недоступен, перегружен или что-то еще пошло не так.", "Session ID": "ID сессии", - "%(senderName)s set a profile picture.": "%(senderName)s установил(а) себе аватар.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s изменил(а) отображаемое имя на %(displayName)s.", + "%(senderName)s set a profile picture.": "%(senderName)s устанавливает себе аватар.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s меняет отображаемое имя на %(displayName)s.", "Signed Out": "Выполнен выход", "This room is not accessible by remote Matrix servers": "Это комната недоступна из других серверов Matrix", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Попытка загрузить выбранный интервал истории чата этой комнаты не удалась, так как у вас нет разрешений на просмотр.", @@ -696,7 +696,7 @@ "This room is not public. You will not be able to rejoin without an invite.": "Эта комната не является публичной. Вы не сможете войти без приглашения.", "Community IDs cannot be empty.": "ID сообществ не могут быть пустыми.", "In reply to ": "В ответ на ", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s изменил(а) отображаемое имя на %(displayName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s меняет отображаемое имя на %(displayName)s.", "Failed to set direct chat tag": "Не удалось установить тег прямого чата", "Failed to remove tag %(tagName)s from room": "Не удалось удалить тег %(tagName)s из комнаты", "Failed to add tag %(tagName)s to room": "Не удалось добавить тег %(tagName)s в комнату", @@ -977,7 +977,7 @@ "Render simple counters in room header": "Отображать простые счетчики в заголовке комнаты", "Enable Emoji suggestions while typing": "Включить предложения смайликов при наборе", "Show a placeholder for removed messages": "Показывать плашки вместо удалённых сообщений", - "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о вступлении | выходе (не влияет на приглашения, исключения и запреты)", + "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о входе/выходе (не влияет на приглашения, кики и баны)", "Show avatar changes": "Показывать изменения аватара", "Show display name changes": "Показывать изменения отображаемого имени", "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Напоминать включить Безопасное Восстановление Сообщений в зашифрованных комнатах", @@ -1639,7 +1639,7 @@ "This alias is already in use": "Этот псевдоним уже используется", "Close dialog": "Закрыть диалог", "Please enter a name for the room": "Пожалуйста, введите название комнаты", - "This room is private, and can only be joined by invitation.": "Эта комната приватная и может быть присоединена только по приглашению.", + "This room is private, and can only be joined by invitation.": "Эта комната приватная, в неё можно войти только по приглашению.", "Hide advanced": "Скрыть расширения", "Show advanced": "Показать расширения", "Please fill why you're reporting.": "Пожалуйста, заполните, почему вы сообщаете.", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index e85732ed82..0fe60e4321 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -540,7 +540,7 @@ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Pri zobrazovaní časových značiek používať 12 hodinový formát (napr. 2:30pm)", "Use compact timeline layout": "Použiť kompaktné rozloženie časovej osy", "Enable automatic language detection for syntax highlighting": "Povoliť automatickú detegciu jazyka pre zvýrazňovanie syntaxe", - "Automatically replace plain text Emoji": "Automaticky nahrádzať textové Emoji", + "Automatically replace plain text Emoji": "Automaticky nahrádzať textové emotikony modernými emoji", "Mirror local video feed": "Zrkadliť lokálne video", "Light theme": "Svetlý vzhľad", "Dark theme": "Tmavý vzhľad", @@ -627,7 +627,7 @@ "Passphrase must not be empty": "Heslo nesmie byť prázdne", "Export room keys": "Exportovať kľúče miestností", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.", - "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.": "Tento súbor umožní komukoľvek, k to má ku nemu prístup dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.", + "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.": "Tento súbor umožní komukoľvek, kto má ku nemu prístup, dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.", "Enter passphrase": "Zadajte (dlhé) heslo", "Confirm passphrase": "Potvrďte heslo", "Export": "Exportovať", @@ -797,7 +797,7 @@ "All Rooms": "Vo všetkych miestnostiach", "State Key": "State Key", "Wednesday": "Streda", - "Quote": "Citácia", + "Quote": "Citovať", "Send logs": "Zahrnúť záznamy", "All messages": "Všetky správy", "Call invitation": "Audio / Video hovory", @@ -814,7 +814,7 @@ "Invite to this room": "Pozvať do tejto miestnosti", "You cannot delete this message. (%(code)s)": "Nemôžete vymazať túto správu. (%(code)s)", "Thursday": "Štvrtok", - "I understand the risks and wish to continue": "Rozumiem rizikám a želám si pokračovať", + "I understand the risks and wish to continue": "Rozumiem riziku a chcem pokračovať", "Logs sent": "Záznamy boli odoslané", "Back": "Naspäť", "Reply": "Odpovedať", @@ -1018,7 +1018,7 @@ "Failed to decrypt %(failedCount)s sessions!": "Nepodarilo sa dešifrovať %(failedCount)s relácií!", "Restored %(sessionCount)s session keys": "Obnovených %(sessionCount)s kľúčov relácií", "Enter Recovery Passphrase": "Zadajte heslo bezpečného obnovenia", - "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Zabezpečte svoju komunikáciu a prístup k šifrovanej histórii konverzácií zadaním hesla obnovenia.", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Získajte prístup k šifrovanej histórií správ a nastavte šiforvanú komunikáciu zadaním vášho (dlhého) hesla obnovenia.", "Waiting for %(userId)s to confirm...": "Čakanie na potvrdenie od používateľa %(userId)s…", "Prompt before sending invites to potentially invalid matrix IDs": "Upozorniť pred odoslaním pozvaní na potenciálne neexistujúce Matrix ID", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Nie je možné nájsť používateľský profil pre Matrix ID zobrazené nižšie. Chcete ich napriek tomu pozvať?", @@ -1029,7 +1029,7 @@ "Enter Recovery Key": "Zadajte kľúč obnovenia", "This looks like a valid recovery key!": "Zdá sa, že toto je platný kľúč obnovenia!", "Not a valid recovery key": "Neplatný kľúč obnovenia", - "Access your secure message history and set up secure messaging by entering your recovery key.": "Zabezpečte svoju komunikáciu a prístup k šifrovanej histórii konverzácií zadaním kľúča obnovenia.", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Získajte prístup k šifrovanej histórií správ a nastavte šiforvanú komunikáciu zadaním vášho kľúča obnovenia.", "Set a new status...": "Nastaviť nový stav…", "Clear status": "Zrušiť stav", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Ste správcom tejto komunity. Nebudete môcť znovu vstúpiť bez pozvania od iného správcu.", @@ -1041,8 +1041,8 @@ "Great! This passphrase looks strong enough.": "Výborne! Toto je dostatočne silné heslo.", "Enter a passphrase...": "Zadajte heslo…", "That matches!": "Zhoda!", - "That doesn't match.": "Nezhodujú sa.", - "Go back to set it again.": "Vráťte sa späť a nastavte znovu.", + "That doesn't match.": "To sa nezhoduje.", + "Go back to set it again.": "Vráťte sa späť a nastavte to znovu.", "Repeat your passphrase...": "Zopakujte heslo…", "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Ak zabudnete svoje heslo obnovenia, tento kľúč môžete použiť ako ďalší bezpečnostný prvok na obnovenie histórii šifrovaných konverzácií.", "As a safety net, you can use it to restore your encrypted message history.": "Tento kľúč môžete použiť ako ďalší bezpečnostný prvok na obnovenie histórii šifrovaných konverzácií.", @@ -1059,8 +1059,8 @@ "Retry": "Skúsiť znovu", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Ak si nenastavíte Bezpečné obnovenie správ, po odhlásení stratíte prístup k histórii šifrovaných konverzácií.", "If you don't want to set this up now, you can later in Settings.": "Ak nechcete pokračovať v nastavení teraz, môžete sa k tomu vrátiť neskôr v časti nastavenia.", - "New Recovery Method": "Nový spôsob obnovi", - "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ak ste si nenastavili nový spôsob obnovenia útočníci sa môžu pokúsiť dostať k vášmu účtu. Ihneď si v nastaveniach zmeňte heslo a znovu si nastavte možnosti obnovenia.", + "New Recovery Method": "Nový spôsob obnovy", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ak ste si nenastavili nový spôsob obnovenia, je možné, že útočník sa pokúša dostať k vášmu účtu. Radšej si ihneď zmeňte vaše heslo a nastavte si nový spôsob obnovenia v Nastaveniach.", "Set up Secure Messages": "Nastaviť bezpečné obnovenie správ", "Go to Settings": "Otvoriť nastavenia", "Whether or not you're logged in (we don't record your username)": "Či ste alebo nie ste prihlásení (nezaznamenávame vaše meno používateľa)", @@ -1089,14 +1089,14 @@ "The user must be unbanned before they can be invited.": "Tomuto používateľovi musíte pred odoslaním pozvania povoliť vstup.", "Group & filter rooms by custom tags (refresh to apply changes)": "Zoskupiť a filtrovať miestnosti podľa vlastných značiek (zmeny sa prejavia po obnovení stránky)", "Render simple counters in room header": "Zobraziť jednoduchú štatistiku v záhlaví miestnosti", - "Enable Emoji suggestions while typing": "Umožniť automatické návrhy Emoji počas písania", + "Enable Emoji suggestions while typing": "Umožniť automatické návrhy emoji počas písania", "Show a placeholder for removed messages": "Zobrazovať náhrady za odstránené správy", "Show join/leave messages (invites/kicks/bans unaffected)": "Zobrazovať správy o vstupe a opustení miestnosti (Nemá vplyv na pozvania/vykázania/zákazy vstupu)", "Show avatar changes": "Zobrazovať zmeny obrázka v profile", "Show display name changes": "Zobrazovať zmeny zobrazovaného mena", "Show read receipts sent by other users": "Zobrazovať potvrdenia o prečítaní od ostatných používateľov", "Show avatars in user and room mentions": "Pri zmienkach používateľov a miestností zobrazovať aj obrázok", - "Enable big emoji in chat": "Povoliť veľké Emoji v konverzáciách", + "Enable big emoji in chat": "Povoliť veľké emoji v konverzáciách", "Send typing notifications": "Posielať oznámenia, keď píšete", "Enable Community Filter Panel": "Povoliť panel filter komunít", "Allow Peer-to-Peer for 1:1 calls": "Povoliť P2P počas priamych audio/video hovorov", @@ -1107,7 +1107,7 @@ "You've successfully verified this user.": "Úspešne ste overili tohoto používateľa.", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Zabezpečené správi s týmto používateľom sú E2E šifrované, čo znamená, že čítanie tretími stranami nie je možné.", "Got It": "Rozumiem", - "Verify this user by confirming the following emoji appear on their screen.": "Overte tohoto používateľa tým, že zistíte, či sa na jeho obrazovke objaví nasledujúci emoji.", + "Verify this user by confirming the following emoji appear on their screen.": "Overte tohto používateľa tak, že zistíte, či sa na jeho obrazovke objavia nasledujúce emoji.", "Verify this user by confirming the following number appears on their screen.": "Overte tohoto používateľa tým, že zistíte, či sa na jeho obrazovke objaví nasledujúce číslo.", "Unable to find a supported verification method.": "Nie je možné nájsť podporovanú metódu overenia.", "Dog": "Hlava psa", @@ -1321,7 +1321,7 @@ "Premium hosting for organisations Learn more": "Platený hosting pre organizácie Zistiť viac", "Other": "Ďalšie", "Find other public servers or use a custom server": "Nájdite ďalšie verejné domovské servery alebo nastavte pripojenie k serveru ručne", - "Please install Chrome, Firefox, or Safari for the best experience.": "Pre najlepší zážitok si prosím nainštalujte Chrome, Firefox alebo Safari.", + "Please install Chrome, Firefox, or Safari for the best experience.": "Prosím, nainštalujte si Chrome, Firefox alebo Safari pre najlepší zážitok.", "Couldn't load page": "Nie je možné načítať stránku", "Want more than a community? Get your own server": "Chceli by ste viac než komunitu? Získajte vlastný server", "This homeserver does not support communities": "Tento domovský server nepodporuje komunity", @@ -1336,10 +1336,10 @@ "Create account": "Vytvoriť účet", "Registration has been disabled on this homeserver.": "Na tomto domovskom servery nie je povolená registrácia.", "Unable to query for supported registration methods.": "Nie je možné požiadať o podporované metódy registrácie.", - "Create your account": "Vytvoriť váš účet", + "Create your account": "Vytvorte si váš účet", "Keep going...": "Pokračujte…", "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "Zašifrovanú kópiu vašich šifrovacích kľúčov uchováme na domovskom servery. Zabezpečte si zálohovanie zadaním hesla obnovenia, čo posilní ochranu vašich údajov.", - "For maximum security, this should be different from your account password.": "Aby ste zachovali maximálnu mieru zabezpečenia, heslo obnovenia by malo byť odlišné ako heslo, ktorým sa prihlasujete do Matrix účtu.", + "For maximum security, this should be different from your account password.": "Aby ste zachovali maximálnu mieru zabezpečenia, (dlhé) heslo by malo byť odlišné od hesla, ktorým sa prihlasujete do vášho účtu.", "Set up with a Recovery Key": "Nastaviť použitím kľúča obnovenia", "Please enter your passphrase a second time to confirm.": "Prosím zadajte heslo obnovenia ešte raz pre potvrdenie.", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Kľúč obnovenia je bezpečnostný mechanizmus - môžete ho použiť na prístup k šifrovacím kľúčom v prípade, ak zabudnete vaše heslo obnovenia.", @@ -1348,10 +1348,10 @@ "Confirm your passphrase": "Potvrdiť heslo obnovenia", "Recovery key": "Kľúč obnovenia", "Starting backup...": "Začína sa zálohovanie…", - "Success!": "Hotovo!", - "A new recovery passphrase and key for Secure Messages have been detected.": "Boli zistené nový kľúč a nové heslo obnovenia zálohovania šifrovacích kľúčov.", + "Success!": "Úspech!", + "A new recovery passphrase and key for Secure Messages have been detected.": "Nové (dlhé) heslo na obnovu zálohy a kľúč pre bezpečné správy boli spozorované.", "Recovery Method Removed": "Odstránený spôsob obnovenia", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ak ste spôsob obnovenia neodstránili vy, útočník sa pravdepodobne usiluje dostať k vašemu účtu. Zmente si prosím heslo na prihlásenie do Matrix účtu a znovu si ihneď nastavte možnosti obnovenia.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ak ste neodstránili spôsob obnovenia vy, je možné, že útočník sa pokúša dostať k vášmu účtu. Radšej si ihneď zmeňte vaše heslo a nastavte si nový spôsob obnovenia v Nastaveniach.", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Či používate alebo nie funkcionalitu známu ako „omrvinky“ (obrázky nad zoznamom miestností)", "Call failed due to misconfigured server": "Hovor zlyhal kvôli nesprávne nakonfigurovanému serveru", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Prosím, požiadajte správcu vášho domovského servera (%(homeserverDomain)s) aby nakonfiguroval Turn server, čo zlepší spoľahlivosť audio / video hovorov.", @@ -1408,9 +1408,9 @@ "Add Email Address": "Pridať emailovú adresu", "Add Phone Number": "Pridať telefónne číslo", "Send cross-signing keys to homeserver": "Poslať kľúče pre podpisovanie naprieč zariadeniami na domovský server", - "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Táto akcia si vyžaduje mať overenú emailovú adresu alebo telefónne číslo cez predvolený server totožností , ale server nezverejnil podmienky používania.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Táto akcia vyžaduje prístup k predvolenému serveru totožností na overenie emailovej adresy alebo telefónneho čísla, ale server nemá žiadne podmienky používania.", "Trust": "Dôverovať", - "Custom (%(level)s)": "Vlastná (%(level)s)", + "Custom (%(level)s)": "Vlastný (%(level)s)", "Sends a message as plain text, without interpreting it as markdown": "Odošle správu vo formáte obyčajný text, bez prekladu markdown", "You do not have the required permissions to use this command.": "Na použitie tohoto príkazu nemáte dostatočné povolenia.", "Error upgrading room": "Chyba pri aktualizácii miestnosti", @@ -1441,7 +1441,7 @@ "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s zmenil pravidlo zakázať vstúpiť z domovských serverov pôvodne sa zhodujúcich s %(oldGlob)s na servery zhodujúce sa s %(newGlob)s, dôvod: %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo zakázať vstúpiť pôvodne sa zhodujúce s %(oldGlob)s na %(newGlob)s, dôvod: %(reason)s", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", - "Multiple integration managers": "Viacej integračných serverov", + "Multiple integration managers": "Viac integračných serverov", "Try out new ways to ignore people (experimental)": "Vyskúšajte si nový spôsob ignorovania používateľov (experiment)", "Enable local event indexing and E2EE search (requires restart)": "Povoliť lokálne indexovanie udalostí a vyhľadávanie v šifrovaných miestnostiach", "Match system theme": "Prispôsobiť sa vzhľadu systému", @@ -1452,12 +1452,12 @@ "The message you are trying to send is too large.": "Správa, ktorú sa usilujete odoslať, je príliš veľká.", "This is your list of users/servers you have blocked - don't leave the room!": "Toto je zoznam používateľov / serverov, ktorých ste zablokovali - neopúšťajte miestnosť!", "Upload": "Nahrať", - "Cross-signing and secret storage are enabled.": "Podpisovanie naprieč zariadeniami a bezpečné úložisko sú aktívne.", - "Cross-signing and secret storage are not yet set up.": "Podpisovanie naprieč zariadeniami a bezpečné úložisko zatiaľ nie sú nastavené.", + "Cross-signing and secret storage are enabled.": "Krížové podpisovanie a bezpečné úložisko sú zapnuté.", + "Cross-signing and secret storage are not yet set up.": "Krížové podpisovanie a bezpečné úložisko zatiaľ nie sú nastavené.", "Bootstrap cross-signing and secret storage": "Zaviesť podpisovanie naprieč zariadeniami a bezpečné úložisko", - "Cross-signing public keys:": "Verejné kľúče podpisovania naprieč zariadeniami:", + "Cross-signing public keys:": "Verejné kľúče krížového podpisovania:", "not found": "nenájdené", - "Cross-signing private keys:": "Súkromné kľúče podpisovania naprieč zariadeniami:", + "Cross-signing private keys:": "Súkromné kľúče krížového podpisovania:", "in secret storage": "na bezpečnom úložisku", "Secret storage public key:": "Verejný kľúč bezpečného úložiska:", "in account data": "v údajoch účtu", @@ -1475,13 +1475,13 @@ "Disconnect identity server": "Odpojiť server totožností", "Disconnect from the identity server ?": "Naozaj sa chcete odpojiť od servera totožností ?", "Disconnect": "Odpojiť", - "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Pred odpojením zo servera totožností by ste mali z neho odstrániť vaše osobné údaje. Žiaľ, server momentálne nie je dostupný a nie je možné sa k nemu pripojiť.", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Pred odpojením by ste mali odstrániť vaše osobné údaje zo servera totožností . Žiaľ, server totožnosti momentálne nie je dostupný a nie je možné sa k nemu pripojiť.", "You should:": "Mali by ste:", "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "Skontrolovať rozšírenia inštalované vo webovom prehliadači, ktoré by mohli blokovať prístup k serveru totožností (napr. rozšírenie Privacy Badger)", "contact the administrators of identity server ": "Kontaktovať správcu servera totožností ", "wait and try again later": "Počkať a skúsiť znovu neskôr", "Disconnect anyway": "Napriek tomu sa odpojiť", - "You are still sharing your personal data on the identity server .": "na servery máte stále uložené vaše osobné údaje.", + "You are still sharing your personal data on the identity server .": "Stále zdielate vaše osobné údaje so serverom totožnosti .", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Odporúčame, aby ste ešte pred odpojením sa zo servera totožností odstránili vašu emailovú adresu a telefónne číslo.", "Identity Server (%(server)s)": "Server totožností (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Momentálne na vyhľadávanie kontaktov a na možnosť byť nájdení kontaktmi ktorých poznáte používate . Zmeniť server totožností môžete nižšie.", @@ -1496,7 +1496,7 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Použiť integračný server na správu botov, widgetov a balíčkov s nálepkami.", "Manage integrations": "Spravovať integrácie", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integračné servery zhromažďujú údaje nastavení, môžu spravovať widgety, odosielať vo vašom mene pozvánky alebo meniť úroveň moci.", - "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Súhlas s podmienkami používania servera totožností (%(serverName)s), aby ste mohli byť nájdení zadaním emailovej adresy alebo telefónneho čísla.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Súhlaste s podmienkami používania servera totožností (%(serverName)s), aby ste mohli byť nájdení zadaním emailovej adresy alebo telefónneho čísla.", "Discovery": "Objaviť", "Deactivate account": "Deaktivovať účet", "Clear cache and reload": "Vymazať vyrovnávaciu pamäť a načítať znovu", @@ -1531,5 +1531,201 @@ "Keep recovery passphrase in memory for this session": "Ponechať (dlhé) heslo pre obnovu zálohy v pamäti pre túto reláciu", "Enter recovery passphrase": "Zadajte (dlhé) heslo pre obnovu zálohy", "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Nemožno sa dostať do tajného úložiska. Prosím, overte, že ste zadali správne (dlhé) heslo pre obnovu zálohy.", - "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Získajte prístup k vašej zabezpečenej histórií správ a vašemu krížom-podpísanej identite na potvrdenie iných relácií zadaním vášho (dlhého) hesla na obnovu zálohy." + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Získajte prístup k vašej šifrovanej histórií správ a vašej krížom podpísanej identite na potvrdenie iných relácií zadaním vášho (dlhého) hesla na obnovu zálohy.", + "Encryption upgrade available": "Je dostupná aktualizácia šifrovania", + "Set up encryption": "Nastaviť šifrovanie", + "Review where you’re logged in": "Zobraziť, kde ste prihlásený", + "New login. Was this you?": "Nové pihlásenie. Ste to vy?", + "%(name)s is requesting verification": "%(name)s žiada o overenie", + "Sign In or Create Account": "Prihlásiť sa alebo vytvoriť nový účet", + "Use your account or create a new one to continue.": "Použite váš existujúci účet alebo si vytvorte nový, aby ste mohli pokračovať.", + "Create Account": "Vytvoriť účet", + "Sign In": "Prihlásiť sa", + "Sends a message as html, without interpreting it as markdown": "Pošlite správu ako HTML, bez interpretácie v Markdowne", + "Failed to set topic": "Nepodarilo sa nastaviť tému", + "Command failed": "Príkaz zlyhal", + "Could not find user in room": "Nepodarilo sa nájsť používateľa v miestnosti", + "Please supply a widget URL or embed code": "Prosím, zadajte URL widgetu alebo vložte kód", + "Verifies a user, session, and pubkey tuple": "Overí používateľa, reláciu a verejné kľúče", + "Unknown (user, session) pair:": "Neznámy pár (používateľ, relácia):", + "Session already verified!": "Relácia je už overená!", + "WARNING: Session already verified, but keys do NOT MATCH!": "VAROVANIE: Relácia je už overená, ale kľúče sa NEZHODUJÚ!", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options.": "Pokiaľ ste zabudli vaše (dlhé) heslo na obnovu zálohy, môžete použiť váš kľúč na obnovu zálohy alebo nastaviť nové spôsoby obnovy zálohy.", + "Incorrect recovery passphrase": "Nesprávne (dlhé) heslo pre obnovu zálohy", + "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Záloha nemohla byť rozšifrovaná pomocou tohto (dlhého) helsa na obnovu zálohy: prosím, overte, či ste zadali správne (dlhé) helso na obnovu zálohy.", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "VAROVANIE: OVERENIE KĽÚČOV ZLYHALO! Podpisovaný kľúč používateľa %(userId)s a relácia %(deviceId)s je \"%(fprint)s\" čo nezodpovedá zadanému kľúču \"%(fingerprint)s\". Môže to znamenať, že vaša komunikácia je infiltrovaná!", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "Zadaný podpisovací kľúč sa zhoduje s podpisovacím kľúčom od relácie %(deviceId)s používateľa %(userId)s. Relácia je označená ako overená.", + "Displays information about a user": "Zobrazuje informácie o používateľovi", + "Send a bug report with logs": "Zaslať chybové hlásenie so záznamami", + "Opens chat with the given user": "Otvorí konverzáciu s daným používateľom", + "Sends a message to the given user": "Pošle správu danému používateľovi", + "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s zmenil/a meno miestnosti z %(oldRoomName)s na %(newRoomName)s.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s pridal/a alternatívne adresy %(addresses)s pre túto miestnosť.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s pridal/a alternatívnu adresu %(addresses)s pre túto miestnosť.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odstránil/a alternatívne adresy %(addresses)s pre túto miestnosť.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odstránil/a alternatívnu adresu %(addresses)s pre túto miestnosť.", + "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s zmenil/a alternatívne adresy pre túto miestnosť.", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s zmenil/a hlavnú a alternatívnu/e adresy pre túto miestnosť.", + "%(senderName)s changed the addresses for this room.": "%(senderName)s zmenil/a adresy pre túto miestnosť.", + "You signed in to a new session without verifying it:": "Prihlásili ste sa do novej relácie bez jej overenia:", + "Verify your other session using one of the options below.": "Overte svoje ostatné relácie pomocou jednej z nižšie uvedených možností.", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) sa prihlásil do novej relácie bez jej overenia:", + "Ask this user to verify their session, or manually verify it below.": "Poproste tohto používateľa, aby si overil svoju reláciu alebo ju nižšie manuálne overte.", + "Not Trusted": "Nedôveryhodné", + "Manually Verify by Text": "Manuálne overte pomocou textu", + "Interactively verify by Emoji": "Interaktívne overte pomocou emoji", + "Done": "Hotovo", + "a few seconds ago": "pred pár sekundami", + "about a minute ago": "približne pred minutou", + "about an hour ago": "približne pred hodinou", + "about a day ago": "približne deň dozadu", + "a few seconds from now": "o pár sekúnd", + "about a minute from now": "približne o minútu", + "about an hour from now": "približne o hodinu", + "about a day from now": "približne o deň", + "Support adding custom themes": "Umožniť pridávať vlastný vzhľad", + "Enable cross-signing to verify per-user instead of per-session": "Povoliť krížové podpisovanie na overovanie používateľa namiesto overovania jednotlivých relácií", + "Your homeserver does not support cross-signing.": "Váš domovský server nepodporuje krížové podpisovanie.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Váš účet má krížovo podpísanú identitu v bezpečnom úložisku, ale zatiaľ nie je nedôveryhodná pre túto reláciu.", + "Reset cross-signing and secret storage": "Obnoviť krížové podpisovanie a bezpečné úložisko", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individuálne overte každú používateľskú reláciu a označte ju za dôveryhodnú, bez dôvery krížovo podpísaných zariadení.", + "Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Zálohovací kľúč je uložený v bezpečnom úložisku, ale jeho načítanie nie je povolené v tejto relácií. Prosím, zapnite krížové podpisovanie v Experimentoch, aby ste mohli modifikovať stav zálohy.", + "Cross-signing": "Krížové podpisovanie", + "Destroy cross-signing keys?": "Zmazať kľúče pre krížové podpisovanie?", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Zmazanie kľúčov pre krížové podpisovanie je nenávratné. Každý, s kým ste sa overili, bude vidieť bezpečnostné upozornenia. Toto určite nechcete robiť, dokiaľ ste nestratili všetky zariadenia, s ktorými by ste mohli krížovo podpisovať.", + "Clear cross-signing keys": "Zmazať kľúče pre krížové podpisovanie", + "a new cross-signing key signature": "nový podpis kľúča pre krížové podpisovanie", + "a device cross-signing signature": "podpis krížovo podpísaného zariadenia", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Získajte prístup k vašej šifrovanej histórií správ a vašej krížom podpísanej identite pre overenie iných relácií zadaním vášho kľúču obnovy.", + "or another cross-signing capable Matrix client": "alebo iný Matrixový klient podporujúci krížové podpisovanie", + "Removing…": "Odstraňovanie…", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Váš nový účet (%(newAccountId)s) je registrovaný, ale už ste prihlásený pod iným účtom (%(loggedInUserId)s).", + "Continue with previous account": "Pokračovať s predošlým účtom", + "Log in to your new account.": "Prihláste sa do vášho nového účtu.", + "You can now close this window or log in to your new account.": "Teraz môžete toto okno zavrieť alebo sa prihlásiť do vášho nového účtu.", + "Registration Successful": "Úspešná registrácia", + "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Potvrďte svoju identitu overením tohto účtu z jednej z vašich iných relácií, čím mu povolíte prístup k šifrovaným správam.", + "This requires the latest Riot on your other devices:": "Toto vyžaduje najnovší Riot na vašich ostatných zariadeniach:", + "Use Recovery Passphrase or Key": "Použite (dlhé) heslo pre obnovu zálohy alebo kľúč", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Vaša nová relácia je teraz overená. Má prístup k vašim šifrovaným správam a ostatný používatelia ju uvidia ako dôveryhodnú.", + "Your new session is now verified. Other users will see it as trusted.": "Vaša nová relácia je teraz overená. Ostatný používatelia ju uvidia ako dôveryhodnú.", + "Without completing security on this session, it won’t have access to encrypted messages.": "Bez dokončenia overenia nebude mať táto relácia prístup k šifrovaným správam.", + "Go Back": "Späť", + "Failed to re-authenticate due to a homeserver problem": "Opätovná autentifikácia zlyhala kvôli problému domovského servera", + "Failed to re-authenticate": "Opätovná autentifikácia zlyhala", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Znovuzískajte prístup k vášmu účtu a obnovte šifrovacie kľúče uložené v tejto relácií. Bez nich nebudete môcť čítať všetky vaše šifrované správy vo všetkých reláciach.", + "Font scaling": "Škálovanie písma", + "Use IRC layout": "Použiť IRC rozloženie", + "Show info about bridges in room settings": "Zobraziť informácie o mostoch v Nastaveniach miestnosti", + "Font size": "Veľkosť písma", + "Custom font size": "Vlastná veľkosť písma", + "Show typing notifications": "Posielať oznámenia, keď píšete", + "Never send encrypted messages to unverified sessions from this session": "Nikdy neposielať šifrované správy neovereným reláciam z tejto relácie", + "Never send encrypted messages to unverified sessions in this room from this session": "Nikdy neposielať šifrované správy neovereným reláciam v tejto miestnosti z tejto relácie", + "Order rooms by name": "Zoradiť miestnosti podľa názvu", + "Show rooms with unread notifications first": "Zobraziť miestnosti s neprečítanými oznámeniami navrchu", + "Show shortcuts to recently viewed rooms above the room list": "Zobraziť skratky nedávno zobrazených miestnosti nad zoznamom miestností", + "Enable message search in encrypted rooms": "Povoliť vyhľadávanie správ v šifrovaných miestnostiach", + "How fast should messages be downloaded.": "Ako rýchlo sa majú správy sťahovať.", + "Manually verify all remote sessions": "Manuálne overiť všetky relácie", + "IRC display name width": "Šírka zobrazovaného mena IRC", + "Verify this session by completing one of the following:": "Overte túto reláciu dokončením jedného z nasledujúcich:", + "Scan this unique code": "Naskenujte tento jedinečný kód", + "or": "alebo", + "Compare unique emoji": "Porovnajte jedinečnú kombináciu emoji", + "Compare a unique set of emoji if you don't have a camera on either device": "Pokiaľ nemáte na svojich zariadeniach kameru, porovnajte jedinečnú kombináciu emoji", + "Confirm the emoji below are displayed on both sessions, in the same order:": "Potvrďte, že nasledujúce emoji sú zobrazené na oboch reláciach v rovnakom poradí:", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "Relácia, ktorú sa snažíte overiť, nepodporuje overovanie QR kódom a ani pomocou emoji, čo sú funkcie, ktoré Riot podporuje. Skúste použiť iného klienta.", + "QR Code": "QR kód", + "Enter your password to sign in and regain access to your account.": "Prihláste sa zadaním hesla a znovuzískajte prístup k vášmu účtu.", + "Forgotten your password?": "Zabudli ste heslo?", + "Sign in and regain access to your account.": "Prihláste sa a znovuzískajte prístup k vášmu účtu.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Nemôžete sa prihlásiť do vášho účtu. Kontaktujte prosím vášho správcu domovského servera pre viac informácií.", + "You're signed out": "Ste odhlásený", + "Clear personal data": "Zmazať osobné dáta", + "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.": "Varovanie: Vaše osobné údaje (vrátane šifrovacích kľúčov) sú stále uložené v tejto relácií. Zmažte ich, ak chcete túto reláciu zahodiť alebo sa chcete prihlásiť cez iný účet.", + "Command Autocomplete": "Automatické dopĺňanie príkazov", + "Community Autocomplete": "Automatické dopĺňanie skupín", + "DuckDuckGo Results": "Výsledky hľadania DuckDuckGo", + "Emoji Autocomplete": "Automatické dopĺňanie emoji", + "Notification Autocomplete": "Automatické dopĺňanie oznámení", + "Room Autocomplete": "Automatické dopĺňanie miestností", + "User Autocomplete": "Automatické dopĺňanie používateľov", + "Start": "Začať", + "Verify this session by confirming the following number appears on its screen.": "Overte túto reláciu tým, že zistíte, či sa na jeho obrazovke objaví nasledujúce číslo.", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Čakám na overenie od relácie %(deviceName)s (%(deviceId)s)…", + "Waiting for your other session to verify…": "Čakám na overenie od vašej druhej relácie…", + "Waiting for %(displayName)s to verify…": "Čakám na %(displayName)s, kým nás overí…", + "Cancelling…": "Rušenie…", + "They match": "Zhodujú sa", + "They don't match": "Nezhodujú sa", + "To be secure, do this in person or use a trusted way to communicate.": "Aby ste si boli istý, urobte to osobne alebo použite dôveryhodný spôsob komunikácie.", + "Lock": "Zámok", + "If you can't scan the code above, verify by comparing unique emoji.": "Pokiaľ nemôžete kód vyššie skenovať, overte sa porovnaním jedinečnej kombinácie emoji.", + "Verify by comparing unique emoji.": "Overenie porovnaním jedinečnej kombinácie emoji", + "Verify by emoji": "Overte pomocou emoji", + "Compare emoji": "Porovnajte emoji", + "Verify all your sessions to ensure your account & messages are safe": "Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné", + "Review": "Prehliadnuť", + "Later": "Neskôr", + "Upgrade": "Upgradovať", + "Verify": "Overiť", + "Verify yourself & others to keep your chats safe": "Overte seba a ostatných, aby vaše komunikácie boli bezpečné", + "Other users may not trust it": "Ostatný používatelia jej nemusia veriť", + "Verify the new login accessing your account: %(name)s": "Overte nové prihlásenie na váš účet: %(name)s", + "From %(deviceName)s (%(deviceId)s)": "Od %(deviceName)s (%(deviceId)s)", + "This bridge was provisioned by .": "Tento most poskytuje .", + "Room name or address": "Meno alebo adresa miestnosti", + "Joins room with given address": "Pridať sa do miestnosti s danou adresou", + "Unrecognised room address:": "Nerozpoznaná adresa miestnosti:", + "Use the improved room list (in development - refresh to apply changes)": "Použiť vylepšený zoznam miestností (vo vývoji - znovunačítajte stránku pre aplikovanie zmien)", + "This bridge is managed by .": "Tento most spravuje .", + "Workspace: %(networkName)s": "Pracovisko: %(networkName)s", + "Channel: %(channelName)s": "Kanál: %(channelName)s", + "Show less": "Zobraziť menej", + "Show more": "Zobraziť viac", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Zmena hesla reštartuje všetky šifrovacie kľúče pre všetky vaše relácie. Šifrované správy sa stanú nečitateľnými, pokiaľ najprv nevyexportujete vaše kľúče a po zmene ich nenaimportujete. V budúcnosti sa tento proces zjednoduší.", + "well formed": "dobre sformulované", + "unexpected type": "neočakávaný typ", + "in memory": "v pamäti", + "Self signing private key:": "Samo-podpísané súkromné kľúče:", + "cached locally": "cachenuté lokálne", + "not found locally": "nenájdené lokálne", + "User signing private key:": "Používateľom podpísané súkromné kľúče:", + "Session backup key:": "Kľúč na zálohu relácie:", + "Homeserver feature support:": "Funkcie podporované domovským serverom:", + "exists": "existuje", + "Your homeserver does not support session management.": "Váš domovský server nepodporuje správu relácií.", + "Unable to load session list": "Nemožno načítať zoznam relácií", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Potvrďte odstránenie týchto relácií použitím Jednotného prihlásenia na overenie vašej identity.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Potvrďte odstránenie tejto relácie použitím Jednotného prihlásenia na overenie vašej identity.", + "Confirm deleting these sessions": "Potvrdiť odstránenie týchto relácií", + "Click the button below to confirm deleting these sessions.|other": "Stlačením tlačítka potvrdíte zmazanie týchto relácií.", + "Click the button below to confirm deleting these sessions.|one": "Stlačením tlačítka potvrdíte zmazanie tejto relácie.", + "Delete sessions|other": "Zmazať relácie", + "Delete sessions|one": "Zmazať reláciu", + "Delete %(count)s sessions|one": "Zmazať %(count)s reláciu", + "Manage": "Spravovať", + "Securely cache encrypted messages locally for them to appear in search results.": "Bezpečne cachovať šifrované správy lokálne, aby sa mohli zobraziť vo vyhľadávaní.", + "Enable": "Povoliť", + "Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with search components added.": "Riotu chýbajú niektoré komponenty potrebné na bezpečné cachovanie šifrovaných správ lokálne. Pokiaľ chcete experimentovať s touto funkciou, spravte si svoj vlastný Riot Desktop s pridanými vyhľadávacími komponentami.", + "Riot can't securely cache encrypted messages locally while running in a web browser. Use Riot Desktop for encrypted messages to appear in search results.": "Riotu nemôže bezpečne cachovať šifrované správy lokálne keď beží v prehliadači. Použite Riot Desktop, aby sa šifrované správy zobrazili vo vyhľadávaní.", + "This session is backing up your keys. ": "Táto relácia zálohuje vaše kľúče. ", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Táto relácia nezálohuje vaše kľúče, ale už máte jednu existujúcu zálohu z ktorej sa môžete obnoviť a postupne pridávať.", + "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Pred odhlásením pripojte túto reláciu k zálohe kľúčov, aby ste predišli strate kľúčov, ktoré môžu byť len v tejto relácií.", + "Connect this session to Key Backup": "Pripojiť túto reláciu k Zálohe kľúčov", + "Backup has a signature from unknown session with ID %(deviceId)s": "Záloha je podpísaná z neznámej relácie s ID %(deviceId)s", + "Backup has a valid signature from this session": "Záloha má platný podpis z tejto relácie", + "Backup has an invalid signature from this session": "Záloha má neplatný podpis z tejto relácie", + "Backup has a valid signature from verified session ": "Záloha má platný podpis z overenej relácie ", + "Backup has a valid signature from unverified session ": "Záloha má platný podpis z neoverenej relácie ", + "Backup has an invalid signature from verified session ": "Záloha má neplatný podpis z overenej relácie ", + "Backup has an invalid signature from unverified session ": "Záloha má neplatný podpis z neoverenej relácie ", + "Backup is not signed by any of your sessions": "Záloha nemá podpis zo žiadnej vašej relácie", + "This backup is trusted because it has been restored on this session": "Táto záloha je dôveryhodná, lebo už bola načítaná v tejto relácií", + "Your keys are not being backed up from this session.": "Vaše kľúče nie sú zálohované z tejto relácie.", + "Enable desktop notifications for this session": "Povoliť desktopové notifikácie pre túto reláciu", + "Enable audible notifications for this session": "Povoliť zvukové notifikácie pre túto reláciu", + "Size must be a number": "Veľkosť musí byť číslo", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Vlastná veľkosť písma môže byť len v rozmedzí %(min)s pt až %(max)s pt" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 7058ad67b0..2acdf199c2 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2424,5 +2424,51 @@ "Click the button below to confirm setting up encryption.": "Klikoni mbi butonin më poshtë që të ripohoni ujdisjen e fshehtëzimit.", "Dismiss read marker and jump to bottom": "Mos merr parasysh piketë leximi dhe hidhu te fundi", "Jump to oldest unread message": "Hidhu te mesazhi më i vjetër i palexuar", - "Upload a file": "Ngarkoni një kartelë" + "Upload a file": "Ngarkoni një kartelë", + "Font scaling": "Përshkallëzim shkronjash", + "Use IRC layout": "Përdor skemë IRC-je", + "Font size": "Madhësi shkronjash", + "Custom font size": "Madhësi vetjake shkronjash", + "IRC display name width": "Gjerësi shfaqjeje emrash IRC", + "Size must be a number": "Madhësia duhet të jetë një numër", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Madhësia vetjake për shkronjat mund të jetë vetëm mes vlerave %(min)s pt dhe %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Përdor me %(min)s pt dhe %(max)s pt", + "Appearance": "Dukje", + "Create room": "Krijo dhomë", + "Room name or address": "Emër ose adresë dhome", + "Joins room with given address": "Hyhet në dhomën me adresën e dhënë", + "Unrecognised room address:": "Adresë dhome që s’njihet:", + "Help us improve Riot": "Ndihmonani të përmirësojmë Riot-in", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Dërgoni të dhëna anonime përdorimi të cilat na ndihmojnë të përmirësojmë Riot-in. Kjo do të përdorë një cookie.", + "I want to help": "Dua të ndihmoj", + "Your homeserver has exceeded its user limit.": "Shërbyesi juaj Home ka tejkaluar kufijtë e tij të përdorimit.", + "Your homeserver has exceeded one of its resource limits.": "Shërbyesi juaj Home ka tejkaluar një nga kufijtë e tij të burimeve.", + "Contact your server admin.": "Lidhuni me përgjegjësin e shërbyesit tuaj.", + "Ok": "OK", + "Set password": "Caktoni fjalëkalim", + "To return to your account in future you need to set a password": "Që të ktheheni te llogaria juaj në të ardhmen, duhet të caktoni një fjalëkalim", + "Restart": "Rinise", + "Upgrade your Riot": "Përmirësoni Riot-in tuaj", + "A new version of Riot is available!": "Ka gati një version të ri të Riot-it!", + "Use the improved room list (in development - refresh to apply changes)": "Përdorni listën e përmirësuar të dhomave (në zhvillim - që të aplikohen ndryshimet, rifreskojeni)", + "Please verify the room ID or address and try again.": "Ju lutemi, verifikoni ID-në ose adresën e dhomës dhe riprovoni.", + "Room ID or address of ban list": "ID dhome ose adresë prej liste ndalimi", + "To link to this room, please add an address.": "Që të lidhni këtë dhomë, ju lutemi, jepni një adresë.", + "Error creating address": "Gabim në krijim adrese", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Pati një gabim në krijimin e asaj adrese. Mund të mos lejohet nga shërbyesi, ose ndodhi një gabim i përkohshëm.", + "You don't have permission to delete the address.": "S’keni leje të fshini adresën.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Pati një gabim në heqjen e asaj adrese. Mund të mos ekzistojë më, ose ndodhi një gabim i përkohshëm.", + "Error removing address": "Gabim në heqje adrese", + "Categories": "Kategori", + "Room address": "Adresë dhome", + "Please provide a room address": "Ju lutemi, jepni një adresë dhome", + "This address is available to use": "Kjo adresë është e lirë për përdorim", + "This address is already in use": "Kjo adresë është e përdorur tashmë", + "Set a room address to easily share your room with other people.": "Caktoni një adresë dhome që të ndani dhomën tuaj me persona të tjerë.", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Me këtë sesion, keni përdorur më herët një version më të ri të Riot-it. Që të ripërdorni këtë version me fshehtëzim skaj më skaj, do t’ju duhet të bëni daljen dhe të rihyni.", + "Address (optional)": "Adresë (opsionale)", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Të fshihet adresa e dhomës %(alias)s dhe të hiqet %(name)s nga drejtoria?", + "delete the address.": "fshije adresën.", + "Use a different passphrase?": "Të përdoret një frazëkalim tjetër?", + "New version available. Update now.": "Version i ri gati. Përditësojeni tani." } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 3edb02df92..72bd0b10ec 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -1106,7 +1106,7 @@ "Bug reporting": "Felrapportering", "FAQ": "FAQ", "Versions": "Versioner", - "Preferences": "Inställningar", + "Preferences": "Alternativ", "Timeline": "Tidslinje", "Room list": "Rumslista", "Autocomplete delay (ms)": "Autokompletteringsfördröjning (ms)", @@ -1630,5 +1630,6 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End" + "End": "End", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Du har blivit utloggad från alla dina sessioner och kommer inte längre att motta pushnotiser. För att återaktivera pushnotiser, logga in igen på varje enhet." } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index bdef596b88..fb6657d6c1 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -77,7 +77,7 @@ "Email": "е-пошта", "Email address": "Адреса е-пошти", "Failed to send email": "Помилка відправки е-почти", - "Edit": "Редактувати", + "Edit": "Редагувати", "Unpin Message": "Відкріпити повідомлення", "Register": "Зареєструватися", "Rooms": "Кімнати", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 4995eeccb0..0d89863155 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2434,5 +2434,51 @@ "QR Code": "QR Code", "Dismiss read marker and jump to bottom": "取消讀取標記並跳至底部", "Jump to oldest unread message": "跳至最舊的未讀訊息", - "Upload a file": "上傳檔案" + "Upload a file": "上傳檔案", + "Use IRC layout": "使用 IRC 佈局", + "IRC display name width": "IRC 顯示名稱寬度", + "Create room": "建立聊天室", + "Font scaling": "字型縮放", + "Font size": "字型大小", + "Custom font size": "自訂字型大小", + "Size must be a number": "大小必須為數字", + "Custom font size can only be between %(min)s pt and %(max)s pt": "自訂字型大小僅能為 %(min)s 點至 %(max)s 點間", + "Use between %(min)s pt and %(max)s pt": "使用 %(min)s 點至 %(max)s 點間", + "Appearance": "外觀", + "Use the improved room list (in development - refresh to apply changes)": "使用改進的聊天室清單(開發中 - 重新整理以套用變更)", + "Room name or address": "聊天室名稱或地址", + "Joins room with given address": "以給定的地址加入聊天室", + "Unrecognised room address:": "無法識別的聊天室地址:", + "Please verify the room ID or address and try again.": "請驗證聊天室 ID 或地址並再試一次。", + "Room ID or address of ban list": "聊天室 ID 或地址的封鎖清單", + "To link to this room, please add an address.": "要連結到此聊天室,請新增地址。", + "Error creating address": "建立地址錯誤", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "建立該地址時發生錯誤。伺服器可能不允許這麼做,或是有暫時性的問題。", + "You don't have permission to delete the address.": "您沒有刪除地址的權限。", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "移除地址時發生錯誤。它可能已不存在或是有暫時性的問題。", + "Error removing address": "移除地址時發生錯誤", + "Categories": "分類", + "Room address": "聊天室地址", + "Please provide a room address": "請提供聊天室地址", + "This address is available to use": "此地址可用", + "This address is already in use": "此地址已被使用", + "Set a room address to easily share your room with other people.": "設定聊天室地址以輕鬆地與其他夥伴分享。", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "您先前在此工作階段中使用了較新版本的 Riot。要再次與此版本一同使用端到端加密,您必須先登出再登入。", + "Address (optional)": "地址(選擇性)", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "刪除聊天室地址 %(alias)s 並從目錄移除 %(name)s?", + "delete the address.": "刪除地址。", + "Use a different passphrase?": "使用不同的通關密語?", + "Help us improve Riot": "協助我們改善 Riot", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "傳送匿名使用資料以協助我們改善 Riot。這將會使用 cookie。", + "I want to help": "我想要協助", + "Your homeserver has exceeded its user limit.": "您的家伺服器已超過使用者限制。", + "Your homeserver has exceeded one of its resource limits.": "您的家伺服器已超過其中一種資源限制。", + "Contact your server admin.": "聯絡您的伺服器管理員。", + "Ok": "確定", + "Set password": "設定密碼", + "To return to your account in future you need to set a password": "要在日後取回您的帳號,您必須設定密碼", + "Restart": "重新啟動", + "Upgrade your Riot": "升級您的 Riot", + "A new version of Riot is available!": "已有新版的 Riot!", + "New version available. Update now.": "有可用的新版本。立刻更新。" } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index d372c38405..fac7c92b65 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -405,7 +405,7 @@ export default class EventIndex extends EventEmitter { continue; } - console.log("EventIndex: Error crawling events:", e); + console.log("EventIndex: Error crawling using checkpoint:", checkpoint, ",", e); this.crawlerCheckpoints.push(checkpoint); continue; } @@ -507,7 +507,13 @@ export default class EventIndex extends EventEmitter { try { for (let i = 0; i < redactionEvents.length; i++) { const ev = redactionEvents[i]; - await indexManager.deleteEvent(ev.getAssociatedId()); + const eventId = ev.getAssociatedId(); + + if (eventId) { + await indexManager.deleteEvent(eventId); + } else { + console.warn("EventIndex: Redaction event doesn't contain a valid associated event id", ev); + } } const eventsAlreadyAdded = await indexManager.addHistoricEvents( diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 3ba1aab135..5fd28d7c54 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -21,10 +21,8 @@ import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import {MatrixClientPeg} from "../MatrixClientPeg"; -import {AutoDiscovery} from "matrix-js-sdk"; import SettingsStore from "../settings/SettingsStore"; -const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours const KIND_PREFERENCE = [ // Ordered: first is most preferred, last is least preferred. KIND_ACCOUNT, @@ -44,7 +42,6 @@ export class IntegrationManagers { _managers: IntegrationManagerInstance[] = []; _client: MatrixClient; - _wellknownRefreshTimerId: number = null; _primaryManager: IntegrationManagerInstance; constructor() { @@ -55,20 +52,19 @@ export class IntegrationManagers { this.stopWatching(); this._client = MatrixClientPeg.get(); this._client.on("accountData", this._onAccountData); + this._client.on("WellKnown.client", this._setupHomeserverManagers); this._compileManagers(); - setInterval(() => this._setupHomeserverManagers(), HS_MANAGERS_REFRESH_INTERVAL); } stopWatching(): void { if (!this._client) return; this._client.removeListener("accountData", this._onAccountData); - if (this._wellknownRefreshTimerId !== null) clearInterval(this._wellknownRefreshTimerId); + this._client.removeListener("WellKnown.client", this._setupHomeserverManagers); } _compileManagers() { this._managers = []; this._setupConfiguredManager(); - this._setupHomeserverManagers(); this._setupAccountManagers(); } @@ -82,39 +78,31 @@ export class IntegrationManagers { } } - async _setupHomeserverManagers() { - if (!MatrixClientPeg.get()) return; - try { - console.log("Updating homeserver-configured integration managers..."); - const homeserverDomain = MatrixClientPeg.getHomeserverName(); - const discoveryResponse = await AutoDiscovery.getRawClientConfig(homeserverDomain); - if (discoveryResponse && discoveryResponse['m.integrations']) { - let managers = discoveryResponse['m.integrations']['managers']; - if (!Array.isArray(managers)) managers = []; // make it an array so we can wipe the HS managers + async _setupHomeserverManagers(discoveryResponse) { + console.log("Updating homeserver-configured integration managers..."); + if (discoveryResponse && discoveryResponse['m.integrations']) { + let managers = discoveryResponse['m.integrations']['managers']; + if (!Array.isArray(managers)) managers = []; // make it an array so we can wipe the HS managers - console.log(`Homeserver has ${managers.length} integration managers`); + console.log(`Homeserver has ${managers.length} integration managers`); - // Clear out any known managers for the homeserver - // TODO: Log out of the scalar clients - this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER); + // Clear out any known managers for the homeserver + // TODO: Log out of the scalar clients + this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER); - // Now add all the managers the homeserver wants us to have - for (const hsManager of managers) { - if (!hsManager["api_url"]) continue; - this._managers.push(new IntegrationManagerInstance( - KIND_HOMESERVER, - hsManager["api_url"], - hsManager["ui_url"], // optional - )); - } - - this._primaryManager = null; // reset primary - } else { - console.log("Homeserver has no integration managers"); + // Now add all the managers the homeserver wants us to have + for (const hsManager of managers) { + if (!hsManager["api_url"]) continue; + this._managers.push(new IntegrationManagerInstance( + KIND_HOMESERVER, + hsManager["api_url"], + hsManager["ui_url"], // optional + )); } - } catch (e) { - console.error(e); - // Errors during discovery are non-fatal + + this._primaryManager = null; // reset primary + } else { + console.log("Homeserver has no integration managers"); } } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 8df75b2c8b..ea10a027cf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -164,13 +164,6 @@ export const SETTINGS = { supportedLevels: ['account'], default: null, }, - "feature_cross_signing": { - // XXX: We shouldn't be using the feature prefix for non-feature settings. There is an exception - // for this case though as we're converting a feature to a setting for a temporary safety net. - displayName: _td("Enable cross-signing to verify per-user instead of per-session"), - supportedLevels: ['device', 'config'], // we shouldn't use LEVELS_FEATURE for non-features, so copy it here. - default: true, - }, "feature_bridge_state": { isFeature: true, supportedLevels: LEVELS_FEATURE, @@ -180,7 +173,7 @@ export const SETTINGS = { "fontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 16, + default: 15, controller: new FontSizeController(), }, "useCustomFontSize": { diff --git a/src/FontWatcher.js b/src/settings/watchers/FontWatcher.ts similarity index 51% rename from src/FontWatcher.js rename to src/settings/watchers/FontWatcher.ts index 006df202ad..dce9e77e9e 100644 --- a/src/FontWatcher.js +++ b/src/settings/watchers/FontWatcher.ts @@ -14,38 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from './dispatcher/dispatcher'; -import SettingsStore, {SettingLevel} from './settings/SettingsStore'; +import dis from '../../dispatcher/dispatcher'; +import SettingsStore, {SettingLevel} from '../SettingsStore'; +import IWatcher from "./Watcher"; +import { toPx } from '../../utils/units'; -export class FontWatcher { - static MIN_SIZE = 13; - static MAX_SIZE = 20; +export class FontWatcher implements IWatcher { + public static readonly MIN_SIZE = 13; + public static readonly MAX_SIZE = 20; + + private dispatcherRef: string; constructor() { - this._dispatcherRef = null; + this.dispatcherRef = null; } - start() { - this._setRootFontSize(SettingsStore.getValue("fontSize")); - this._dispatcherRef = dis.register(this._onAction); + public start() { + this.setRootFontSize(SettingsStore.getValue("fontSize")); + this.dispatcherRef = dis.register(this.onAction); } - stop() { - dis.unregister(this._dispatcherRef); + public stop() { + dis.unregister(this.dispatcherRef); } - _onAction = (payload) => { + private onAction = (payload) => { if (payload.action === 'update-font-size') { - this._setRootFontSize(payload.size); + this.setRootFontSize(payload.size); } }; - _setRootFontSize = (size) => { + private setRootFontSize = (size) => { const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); - if (fontSize != size) { + if (fontSize !== size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); } - document.querySelector(":root").style.fontSize = fontSize + "px"; + (document.querySelector(":root")).style.fontSize = toPx(fontSize); }; } diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts new file mode 100644 index 0000000000..ce0db881c0 --- /dev/null +++ b/src/settings/watchers/ThemeWatcher.ts @@ -0,0 +1,138 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019, 2020 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 SettingsStore, { SettingLevel } from '../SettingsStore'; +import dis from '../../dispatcher/dispatcher'; +import { Action } from '../../dispatcher/actions'; +import ThemeController from "../controllers/ThemeController"; +import { setTheme } from "../../theme"; +import { ActionPayload } from '../../dispatcher/payloads'; + +export default class ThemeWatcher { + // XXX: I think this is unused. + static _instance = null; + + private themeWatchRef: string; + private systemThemeWatchRef: string; + private dispatcherRef: string; + + private preferDark: MediaQueryList; + private preferLight: MediaQueryList; + + private currentTheme: string; + + constructor() { + this.themeWatchRef = null; + this.systemThemeWatchRef = null; + this.dispatcherRef = null; + + // we have both here as each may either match or not match, so by having both + // we can get the tristate of dark/light/unsupported + this.preferDark = (global).matchMedia("(prefers-color-scheme: dark)"); + this.preferLight = (global).matchMedia("(prefers-color-scheme: light)"); + + this.currentTheme = this.getEffectiveTheme(); + } + + public start() { + this.themeWatchRef = SettingsStore.watchSetting("theme", null, this.onChange); + this.systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this.onChange); + if (this.preferDark.addEventListener) { + this.preferDark.addEventListener('change', this.onChange); + this.preferLight.addEventListener('change', this.onChange); + } + this.dispatcherRef = dis.register(this.onAction); + } + + public stop() { + if (this.preferDark.addEventListener) { + this.preferDark.removeEventListener('change', this.onChange); + this.preferLight.removeEventListener('change', this.onChange); + } + SettingsStore.unwatchSetting(this.systemThemeWatchRef); + SettingsStore.unwatchSetting(this.themeWatchRef); + dis.unregister(this.dispatcherRef); + } + + private onChange = () => { + this.recheck(); + }; + + private onAction = (payload: ActionPayload) => { + if (payload.action === Action.RecheckTheme) { + // XXX forceTheme + this.recheck(payload.forceTheme); + } + }; + + // XXX: forceTheme param added here as local echo appears to be unreliable + // https://github.com/vector-im/riot-web/issues/11443 + public recheck(forceTheme?: string) { + const oldTheme = this.currentTheme; + this.currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; + if (oldTheme !== this.currentTheme) { + setTheme(this.currentTheme); + } + } + + public getEffectiveTheme(): string { + // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab + + // XXX: checking the isLight flag here makes checking it in the ThemeController + // itself completely redundant since we just override the result here and we're + // now effectively just using the ThemeController as a place to store the static + // variable. The system theme setting probably ought to have an equivalent + // controller that honours the same flag, although probablt better would be to + // have the theme logic in one place rather than split between however many + // different places. + if (ThemeController.isLogin) return 'light'; + + // If the user has specifically enabled the system matching option (excluding default), + // then use that over anything else. We pick the lowest possible level for the setting + // to ensure the ordering otherwise works. + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); + if (systemThemeExplicit) { + console.log("returning explicit system theme"); + if (this.preferDark.matches) return 'dark'; + if (this.preferLight.matches) return 'light'; + } + + // If the user has specifically enabled the theme (without the system matching option being + // enabled specifically and excluding the default), use that theme. We pick the lowest possible + // level for the setting to ensure the ordering otherwise works. + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); + if (themeExplicit) { + console.log("returning explicit theme: " + themeExplicit); + return themeExplicit; + } + + // If the user hasn't really made a preference in either direction, assume the defaults of the + // settings and use those. + if (SettingsStore.getValue('use_system_theme')) { + if (this.preferDark.matches) return 'dark'; + if (this.preferLight.matches) return 'light'; + } + console.log("returning theme value"); + return SettingsStore.getValue('theme'); + } + + public isSystemThemeSupported() { + return this.preferDark.matches || this.preferLight.matches; + } +} diff --git a/src/settings/watchers/Watcher.ts b/src/settings/watchers/Watcher.ts new file mode 100644 index 0000000000..a9f6f3f2c8 --- /dev/null +++ b/src/settings/watchers/Watcher.ts @@ -0,0 +1,20 @@ +/* +Copyright 2020 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. +*/ + +export default interface IWatcher { + start(): void + stop(): void +} \ No newline at end of file diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 3d82d086d7..6e5007895c 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -46,7 +46,6 @@ const INITIAL_STATE = { forwardingEvent: null, quotingEvent: null, - matrixClientIsReady: false, }; /** @@ -60,9 +59,6 @@ class RoomViewStore extends Store { // Initialise state this._state = INITIAL_STATE; - if (MatrixClientPeg.get()) { - this._state.matrixClientIsReady = MatrixClientPeg.get().isInitialSyncComplete(); - } } _setState(newState) { @@ -157,11 +153,6 @@ class RoomViewStore extends Store { }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); break; } - case 'sync_state': - this._setState({ - matrixClientIsReady: MatrixClientPeg.get() && MatrixClientPeg.get().isInitialSyncComplete(), - }); - break; } } @@ -224,6 +215,7 @@ class RoomViewStore extends Store { storeRoomAliasInCache(payload.room_alias, result.room_id); roomId = result.room_id; } catch (err) { + console.error("RVS failed to get room id for alias: ", err); dis.dispatch({ action: 'view_room_error', room_id: null, @@ -272,9 +264,8 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); - // XXX: We are relying on the error message returned by browsers here. - // This isn't great, but it does generalize the error being shown to users. - if (msg && msg.startsWith("CORS request rejected")) { + console.log("Failed to join room:", msg); + if (err.name === "ConnectionError") { msg = _t("There was an error joining the room"); } if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { @@ -375,7 +366,7 @@ class RoomViewStore extends Store { } shouldPeek() { - return this._state.shouldPeek && this._state.matrixClientIsReady; + return this._state.shouldPeek; } } diff --git a/src/stores/ToastStore.js b/src/stores/ToastStore.js deleted file mode 100644 index 8901736739..0000000000 --- a/src/stores/ToastStore.js +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2020 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 EventEmitter from 'events'; - -/** - * Holds the active toasts - */ -export default class ToastStore extends EventEmitter { - static PRIORITY_REALTIME = 0; - static PRIORITY_DEFAULT = 1; - static PRIORITY_LOW = 2; - - static sharedInstance() { - if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore(); - return global.mx_ToastStore; - } - - constructor() { - super(); - this._dispatcherRef = null; - this._toasts = []; - } - - reset() { - this._toasts = []; - } - - /** - * Add or replace a toast - * If a toast with the same toastKey already exists, the given toast will replace it - * Toasts are always added underneath any toasts of the same priority, so existing - * toasts stay at the top unless a higher priority one arrives (better to not change the - * toast unless necessary). - * - * @param {boject} newToast The new toast - */ - addOrReplaceToast(newToast) { - if (newToast.priority === undefined) newToast.priority = ToastStore.PRIORITY_DEFAULT; - - const oldIndex = this._toasts.findIndex(t => t.key === newToast.key); - if (oldIndex === -1) { - let newIndex = this._toasts.length; - while (newIndex > 0 && this._toasts[newIndex - 1].priority > newToast.priority) --newIndex; - this._toasts.splice(newIndex, 0, newToast); - } else { - this._toasts[oldIndex] = newToast; - } - this.emit('update'); - } - - dismissToast(key) { - this._toasts = this._toasts.filter(t => t.key !== key); - this.emit('update'); - } - - getToasts() { - return this._toasts; - } -} diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts new file mode 100644 index 0000000000..55c48c3937 --- /dev/null +++ b/src/stores/ToastStore.ts @@ -0,0 +1,93 @@ +/* +Copyright 2020 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 EventEmitter from "events"; +import React, {JSXElementConstructor} from "react"; + +export interface IToast> { + key: string; + // higher priority number will be shown on top of lower priority + priority: number; + title: string; + icon?: string; + component: C; + props?: React.ComponentProps; +} + +/** + * Holds the active toasts + */ +export default class ToastStore extends EventEmitter { + private toasts: IToast[] = []; + // The count of toasts which have been seen & dealt with in this stack + // where the count resets when the stack of toasts clears. + private countSeen = 0; + + static sharedInstance() { + if (!window.mx_ToastStore) window.mx_ToastStore = new ToastStore(); + return window.mx_ToastStore; + } + + reset() { + this.toasts = []; + this.countSeen = 0; + } + + /** + * Add or replace a toast + * If a toast with the same toastKey already exists, the given toast will replace it + * Toasts are always added underneath any toasts of the same priority, so existing + * toasts stay at the top unless a higher priority one arrives (better to not change the + * toast unless necessary). + * + * @param {object} newToast The new toast + */ + addOrReplaceToast>(newToast: IToast) { + const oldIndex = this.toasts.findIndex(t => t.key === newToast.key); + if (oldIndex === -1) { + let newIndex = this.toasts.length; + while (newIndex > 0 && this.toasts[newIndex - 1].priority < newToast.priority) --newIndex; + this.toasts.splice(newIndex, 0, newToast); + } else { + this.toasts[oldIndex] = newToast; + } + this.emit('update'); + } + + dismissToast(key) { + if (this.toasts[0] && this.toasts[0].key === key) { + this.countSeen++; + } + + const length = this.toasts.length; + this.toasts = this.toasts.filter(t => t.key !== key); + if (length !== this.toasts.length) { + if (this.toasts.length === 0) { + this.countSeen = 0; + } + + this.emit('update'); + } + } + + getToasts() { + return this.toasts; + } + + getCountSeen() { + return this.countSeen; + } +} diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts new file mode 100644 index 0000000000..b41d56be3e --- /dev/null +++ b/src/stores/room-list/ListLayout.ts @@ -0,0 +1,71 @@ +/* +Copyright 2020 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 { TagID } from "./models"; + +const TILE_HEIGHT_PX = 34; + +interface ISerializedListLayout { + numTiles: number; +} + +export class ListLayout { + private _n = 0; + + constructor(public readonly tagId: TagID) { + const serialized = localStorage.getItem(this.key); + if (serialized) { + // We don't use the setters as they cause writes. + const parsed = JSON.parse(serialized); + this._n = parsed.numTiles; + } + } + + public get tileHeight(): number { + return TILE_HEIGHT_PX; + } + + private get key(): string { + return `mx_sublist_layout_${this.tagId}_boxed`; + } + + public get visibleTiles(): number { + return Math.max(this._n, this.minVisibleTiles); + } + + public set visibleTiles(v: number) { + this._n = v; + localStorage.setItem(this.key, JSON.stringify(this.serialize())); + } + + public get minVisibleTiles(): number { + return 3; + } + + public tilesToPixels(n: number): number { + return n * this.tileHeight; + } + + public pixelsToTiles(px: number): number { + return px / this.tileHeight; + } + + private serialize(): ISerializedListLayout { + return { + numTiles: this.visibleTiles, + }; + } +} diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index 82a6e841db..f4a56130ca 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -111,6 +111,21 @@ an object containing the tags it needs to worry about and the rooms within. The decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering. +## Filtering + +Filters are provided to the store as condition classes, which are then passed along to the algorithm +implementations. The implementations then get to decide how to actually filter the rooms, however in +practice the base `Algorithm` class deals with the filtering in a more optimized/generic way. + +The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms, +as the old room list store does. When a filter condition changes, it emits an update which (in this +case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a +minor subset where possible to avoid over-iterating rooms. + +All filter conditions are considered "stable" by the consumers, meaning that the consumer does not +expect a change in the condition unless the condition says it has changed. This is intentional to +maintain the caching behaviour described above. + ## Class breakdowns The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 881b8fd3cf..af9970d3cc 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -18,7 +18,7 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; -import { Algorithm } from "./algorithms/list-ordering/Algorithm"; +import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -26,6 +26,9 @@ import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorit import { getListAlgorithmInstance } from "./algorithms/list-ordering"; import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; +import { readReceiptChangeIsFor } from "../../utils/read-receipts"; +import { IFilterCondition } from "./filters/IFilterCondition"; +import { TagWatcher } from "./TagWatcher"; interface IState { tagsEnabled?: boolean; @@ -40,11 +43,13 @@ interface IState { */ export const LISTS_UPDATE_EVENT = "lists_update"; -class _RoomListStore extends AsyncStore { - private matrixClient: MatrixClient; +export class RoomListStore2 extends AsyncStore { + private _matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; private algorithm: Algorithm; + private filterConditions: IFilterCondition[] = []; + private tagWatcher = new TagWatcher(this); private readonly watchedSettings = [ 'RoomList.orderAlphabetically', @@ -64,6 +69,10 @@ class _RoomListStore extends AsyncStore { return this.algorithm.getOrderedRooms(); } + public get matrixClient(): MatrixClient { + return this._matrixClient; + } + // TODO: Remove enabled flag when the old RoomListStore goes away private checkEnabled() { this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); @@ -95,7 +104,7 @@ class _RoomListStore extends AsyncStore { this.checkEnabled(); if (!this.enabled) return; - this.matrixClient = payload.matrixClient; + this._matrixClient = payload.matrixClient; // Update any settings here, as some may have happened before we were logically ready. console.log("Regenerating room lists: Startup"); @@ -110,7 +119,7 @@ class _RoomListStore extends AsyncStore { // Reset state without causing updates as the client will have been destroyed // and downstream code will throw NPE errors. this.reset(null, true); - this.matrixClient = null; + this._matrixClient = null; this.initialListsGenerated = false; // we'll want to regenerate them } @@ -135,15 +144,10 @@ class _RoomListStore extends AsyncStore { if (payload.action === 'MatrixActions.Room.receipt') { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). - // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle - const myUserId = this.matrixClient.getUserId(); - for (const eventId of Object.keys(payload.event.getContent())) { - const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); - if (receiptUsers.includes(myUserId)) { - // TODO: Update room now that it's been read - console.log(payload); - return; - } + if (readReceiptChangeIsFor(payload.event, this.matrixClient)) { + // TODO: Update room now that it's been read + console.log(payload); + return; } } else if (payload.action === 'MatrixActions.Room.tags') { // TODO: Update room from tags @@ -156,8 +160,21 @@ class _RoomListStore extends AsyncStore { const roomId = eventPayload.event.getRoomId(); const room = this.matrixClient.getRoom(roomId); - console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`); - await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); + const tryUpdate = async (updatedRoom: Room) => { + console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`); + await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline); + }; + if (!room) { + console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`); + console.warn(`Queuing failed room update for retry as a result.`); + setTimeout(async () => { + const updatedRoom = this.matrixClient.getRoom(roomId); + await tryUpdate(updatedRoom); + }, 100); // 100ms should be enough for the room to show up + return; + } else { + await tryUpdate(room); + } } else if (payload.action === 'MatrixActions.Event.decrypted') { const eventPayload = (payload); // TODO: Type out the dispatcher types const roomId = eventPayload.event.getRoomId(); @@ -175,11 +192,20 @@ class _RoomListStore extends AsyncStore { // TODO: Update DMs console.log(payload); } else if (payload.action === 'MatrixActions.Room.myMembership') { + // TODO: Improve new room check + const membershipPayload = (payload); // TODO: Type out the dispatcher types + if (!membershipPayload.oldMembership && membershipPayload.membership === "join") { + console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`); + await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom); + } + // TODO: Update room from membership change console.log(payload); } else if (payload.action === 'MatrixActions.Room') { - // TODO: Update room from creation/join - console.log(payload); + // TODO: Improve new room check + // const roomPayload = (payload); // TODO: Type out the dispatcher types + // console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`); + // await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom); } else if (payload.action === 'view_room') { // TODO: Update sticky room console.log(payload); @@ -215,11 +241,22 @@ class _RoomListStore extends AsyncStore { } private setAlgorithmClass() { + if (this.algorithm) { + this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); + } this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); + this.algorithm.setFilterConditions(this.filterConditions); + this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); } + private onAlgorithmListUpdated = () => { + console.log("Underlying algorithm has triggered a list update - refiring"); + this.emit(LISTS_UPDATE_EVENT, this); + }; + private async regenerateAllLists() { console.warn("Regenerating all room lists"); + const tags: ITagSortingMap = {}; for (const tagId of OrderedDefaultTagIDs) { tags[tagId] = this.getSortAlgorithmFor(tagId); @@ -238,16 +275,38 @@ class _RoomListStore extends AsyncStore { this.emit(LISTS_UPDATE_EVENT, this); } + + public addFilter(filter: IFilterCondition): void { + console.log("Adding filter condition:", filter); + this.filterConditions.push(filter); + if (this.algorithm) { + this.algorithm.addFilterCondition(filter); + } + } + + public removeFilter(filter: IFilterCondition): void { + console.log("Removing filter condition:", filter); + const idx = this.filterConditions.indexOf(filter); + if (idx >= 0) { + this.filterConditions.splice(idx, 1); + + if (this.algorithm) { + this.algorithm.removeFilterCondition(filter); + } + } + } } export default class RoomListStore { - private static internalInstance: _RoomListStore; + private static internalInstance: RoomListStore2; - public static get instance(): _RoomListStore { + public static get instance(): RoomListStore2 { if (!RoomListStore.internalInstance) { - RoomListStore.internalInstance = new _RoomListStore(); + RoomListStore.internalInstance = new RoomListStore2(); } return RoomListStore.internalInstance; } } + +window.mx_RoomListStore2 = RoomListStore.instance; diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts new file mode 100644 index 0000000000..1fb5223e00 --- /dev/null +++ b/src/stores/room-list/TagWatcher.ts @@ -0,0 +1,80 @@ +/* +Copyright 2020 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 { RoomListStore2 } from "./RoomListStore2"; +import TagOrderStore from "../TagOrderStore"; +import { CommunityFilterCondition } from "./filters/CommunityFilterCondition"; +import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; + +/** + * Watches for changes in tags/groups to manage filters on the provided RoomListStore + */ +export class TagWatcher { + // TODO: Support custom tags, somehow (deferred to later work - need support elsewhere) + private filters = new Map(); + + constructor(private store: RoomListStore2) { + TagOrderStore.addListener(this.onTagsUpdated); + } + + private onTagsUpdated = () => { + const lastTags = Array.from(this.filters.keys()); + const newTags = TagOrderStore.getSelectedTags(); + + if (arrayHasDiff(lastTags, newTags)) { + // Selected tags changed, do some filtering + + if (!this.store.matrixClient) { + console.warn("Tag update without an associated matrix client - ignoring"); + return; + } + + const newFilters = new Map(); + + // TODO: Support custom tags properly + const filterableTags = newTags.filter(t => t.startsWith("+")); + + for (const tag of filterableTags) { + const group = this.store.matrixClient.getGroup(tag); + if (!group) { + console.warn(`Group selected with no group object available: ${tag}`); + continue; + } + + newFilters.set(tag, new CommunityFilterCondition(group)); + } + + // Update the room list store's filters + const diff = arrayDiff(lastTags, newTags); + for (const tag of diff.added) { + // TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters) + const filter = newFilters.get(tag); + if (!filter) continue; + + this.store.addFilter(filter); + } + for (const tag of diff.removed) { + // TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters) + const filter = this.filters.get(tag); + if (!filter) continue; + + this.store.removeFilter(filter); + } + + this.filters = newFilters; + } + }; +} diff --git a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts index e154847847..3921bb6221 100644 --- a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts @@ -20,24 +20,138 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; import { ITagMap, ITagSortingMap } from "../models"; import DMRoomMap from "../../../../utils/DMRoomMap"; +import { FILTER_CHANGED, IFilterCondition } from "../../filters/IFilterCondition"; +import { EventEmitter } from "events"; // TODO: Add locking support to avoid concurrent writes? -// TODO: EventEmitter support? Might not be needed. + +/** + * Fired when the Algorithm has determined a list has been updated. + */ +export const LIST_UPDATED_EVENT = "list_updated_event"; /** * Represents a list ordering algorithm. This class will take care of tag * management (which rooms go in which tags) and ask the implementation to * deal with ordering mechanics. */ -export abstract class Algorithm { - protected cached: ITagMap = {}; +export abstract class Algorithm extends EventEmitter { + private _cachedRooms: ITagMap = {}; + private filteredRooms: ITagMap = {}; + protected sortAlgorithms: ITagSortingMap; protected rooms: Room[] = []; protected roomIdsToTags: { [roomId: string]: TagID[]; } = {}; + protected allowedByFilter: Map = new Map(); + protected allowedRoomsByFilters: Set = new Set(); protected constructor() { + super(); + } + + protected get hasFilters(): boolean { + return this.allowedByFilter.size > 0; + } + + protected set cachedRooms(val: ITagMap) { + this._cachedRooms = val; + this.recalculateFilteredRooms(); + } + + protected get cachedRooms(): ITagMap { + return this._cachedRooms; + } + + /** + * Sets the filter conditions the Algorithm should use. + * @param filterConditions The filter conditions to use. + */ + public setFilterConditions(filterConditions: IFilterCondition[]): void { + for (const filter of filterConditions) { + this.addFilterCondition(filter); + } + } + + public addFilterCondition(filterCondition: IFilterCondition): void { + // Populate the cache of the new filter + this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r))); + this.recalculateFilteredRooms(); + filterCondition.on(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this)); + } + + public removeFilterCondition(filterCondition: IFilterCondition): void { + filterCondition.off(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this)); + if (this.allowedByFilter.has(filterCondition)) { + this.allowedByFilter.delete(filterCondition); + + // If we removed the last filter, tell consumers that we've "updated" our filtered + // view. This will trick them into getting the complete room list. + if (!this.hasFilters) { + this.emit(LIST_UPDATED_EVENT); + } + } + } + + protected recalculateFilteredRooms() { + if (!this.hasFilters) { + return; + } + + console.warn("Recalculating filtered room list"); + const allowedByFilters = new Set(); + const filters = Array.from(this.allowedByFilter.keys()); + const newMap: ITagMap = {}; + for (const tagId of Object.keys(this.cachedRooms)) { + // Cheaply clone the rooms so we can more easily do operations on the list. + // We optimize our lookups by trying to reduce sample size as much as possible + // to the rooms we know will be deduped by the Set. + const rooms = this.cachedRooms[tagId]; + const remainingRooms = rooms.map(r => r).filter(r => !allowedByFilters.has(r)); + const allowedRoomsInThisTag = []; + for (const filter of filters) { + const filteredRooms = remainingRooms.filter(r => filter.isVisible(r)); + for (const room of filteredRooms) { + const idx = remainingRooms.indexOf(room); + if (idx >= 0) remainingRooms.splice(idx, 1); + allowedByFilters.add(room); + allowedRoomsInThisTag.push(room); + } + } + newMap[tagId] = allowedRoomsInThisTag; + console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`); + } + + this.allowedRoomsByFilters = allowedByFilters; + this.filteredRooms = newMap; + this.emit(LIST_UPDATED_EVENT); + } + + protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void { + const filters = this.allowedByFilter.keys(); + for (const room of added) { + for (const filter of filters) { + if (filter.isVisible(room)) { + this.allowedRoomsByFilters.add(room); + break; + } + } + } + + // Now that we've updated the allowed rooms, recalculate the tag + this.recalculateFilteredRoomsForTag(tagId); + } + + protected recalculateFilteredRoomsForTag(tagId: TagID): void { + console.log(`Recalculating filtered rooms for ${tagId}`); + delete this.filteredRooms[tagId]; + const rooms = this.cachedRooms[tagId]; + const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r)); + if (filteredRooms.length > 0) { + this.filteredRooms[tagId] = filteredRooms; + } + console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); } /** @@ -54,12 +168,15 @@ export abstract class Algorithm { } /** - * Gets an ordered set of rooms for the all known tags. + * Gets an ordered set of rooms for the all known tags, filtered. * @returns {ITagMap} The cached list of rooms, ordered, * for each tag. May be empty, but never null/undefined. */ public getOrderedRooms(): ITagMap { - return this.cached; + if (!this.hasFilters) { + return this.cachedRooms; + } + return this.filteredRooms; } /** @@ -83,7 +200,7 @@ export abstract class Algorithm { // If we can avoid doing work, do so. if (!rooms.length) { await this.generateFreshTags(newTags); // just in case it wants to do something - this.cached = newTags; + this.cachedRooms = newTags; return; } @@ -130,7 +247,7 @@ export abstract class Algorithm { await this.generateFreshTags(newTags); - this.cached = newTags; + this.cachedRooms = newTags; this.updateTagsFromCache(); } @@ -140,9 +257,9 @@ export abstract class Algorithm { protected updateTagsFromCache() { const newMap = {}; - const tags = Object.keys(this.cached); + const tags = Object.keys(this.cachedRooms); for (const tagId of tags) { - const rooms = this.cached[tagId]; + const rooms = this.cachedRooms[tagId]; for (const room of rooms) { if (!newMap[room.roomId]) newMap[room.roomId] = []; newMap[room.roomId].push(tagId); diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index c72cdc2e1c..6c4498dad3 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -17,7 +17,7 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RoomUpdateCause, TagID } from "../../models"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; import * as Unread from '../../../../Unread'; @@ -92,9 +92,9 @@ export class ImportanceAlgorithm extends Algorithm { // can be found from `this.indices[tag][category]` and the sticky room information // from `this.stickyRoom`. // - // The room list store is always provided with the `this.cached` results, which are + // The room list store is always provided with the `this.cachedRooms` results, which are // updated as needed and not recalculated often. For example, when a room needs to - // move within a tag, the array in `this.cached` will be spliced instead of iterated. + // move within a tag, the array in `this.cachedRooms` will be spliced instead of iterated. // The `indices` help track the positions of each category to make splicing easier. private indices: { @@ -189,7 +189,13 @@ export class ImportanceAlgorithm extends Algorithm { } public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - const tags = this.roomIdsToTags[room.roomId]; + if (cause === RoomUpdateCause.NewRoom) { + // TODO: Be smarter and insert rather than regen the planet. + await this.setKnownRooms([room, ...this.rooms]); + return; + } + + let tags = this.roomIdsToTags[room.roomId]; if (!tags) { console.warn(`No tags known for "${room.name}" (${room.roomId})`); return false; @@ -201,7 +207,7 @@ export class ImportanceAlgorithm extends Algorithm { continue; // Nothing to do here. } - const taggedRooms = this.cached[tag]; + const taggedRooms = this.cachedRooms[tag]; const indices = this.indices[tag]; let roomIdx = taggedRooms.indexOf(room); if (roomIdx === -1) { diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 44a501e592..e129e98e6f 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -49,7 +49,7 @@ export class NaturalAlgorithm extends Algorithm { for (const tag of tags) { // TODO: Optimize this loop to avoid useless operations // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags - this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]); + this.cachedRooms[tag] = await sortRoomsWithAlgorithm(this.cachedRooms[tag], tag, this.sortAlgorithms[tag]); } return true; // assume we changed something } diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts new file mode 100644 index 0000000000..13d0084ae1 --- /dev/null +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -0,0 +1,58 @@ +/* +Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room"; +import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition"; +import { Group } from "matrix-js-sdk/src/models/group"; +import { EventEmitter } from "events"; +import GroupStore from "../../GroupStore"; +import { arrayHasDiff } from "../../../utils/arrays"; +import { IDisposable } from "../../../utils/IDisposable"; + +/** + * A filter condition for the room list which reveals rooms which + * are a member of a given community. + */ +export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDisposable { + private roomIds: string[] = []; + + constructor(private community: Group) { + super(); + GroupStore.on("update", this.onStoreUpdate); + + // noinspection JSIgnoredPromiseFromCall + this.onStoreUpdate(); // trigger a false update to seed the store + } + + public isVisible(room: Room): boolean { + return this.roomIds.includes(room.roomId); + } + + private onStoreUpdate = async (): Promise => { + // We don't actually know if the room list changed for the community, so just + // check it again. + const beforeRoomIds = this.roomIds; + this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId); + if (arrayHasDiff(beforeRoomIds, this.roomIds)) { + console.log("Updating filter for group: ", this.community.groupId); + this.emit(FILTER_CHANGED); + } + }; + + public dispose(): void { + GroupStore.off("update", this.onStoreUpdate); + } +} diff --git a/src/stores/room-list/filters/IFilterCondition.ts b/src/stores/room-list/filters/IFilterCondition.ts new file mode 100644 index 0000000000..f7f0f61194 --- /dev/null +++ b/src/stores/room-list/filters/IFilterCondition.ts @@ -0,0 +1,42 @@ +/* +Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room"; +import { EventEmitter } from "events"; + +export const FILTER_CHANGED = "filter_changed"; + +/** + * A filter condition for the room list, determining if a room + * should be shown or not. + * + * All filter conditions are expected to be stable executions, + * meaning that given the same input the same answer will be + * returned (thus allowing caching). As such, filter conditions + * can, but shouldn't, do heavier logic and not worry about being + * called constantly by the room list. When the condition changes + * such that different inputs lead to different answers (such + * as a change in the user's input), this emits FILTER_CHANGED. + */ +export interface IFilterCondition extends EventEmitter { + /** + * Determines if a given room should be visible under this + * condition. + * @param room The room to check. + * @returns True if the room should be visible. + */ + isVisible(room: Room): boolean; +} diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts new file mode 100644 index 0000000000..6a76569df3 --- /dev/null +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -0,0 +1,46 @@ +/* +Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room"; +import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition"; +import { EventEmitter } from "events"; + +/** + * A filter condition for the room list which reveals rooms of a particular + * name, or associated name (like a room alias). + */ +export class NameFilterCondition extends EventEmitter implements IFilterCondition { + private _search = ""; + + constructor() { + super(); + } + + public get search(): string { + return this._search; + } + + public set search(val: string) { + this._search = val; + console.log("Updating filter for room name search:", this._search); + this.emit(FILTER_CHANGED); + } + + public isVisible(room: Room): boolean { + // TODO: Improve this filter to include aliases and such + return room.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0; + } +} diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index a0c2621077..9a27569db4 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -39,4 +39,5 @@ export type TagID = string | DefaultTagID; export enum RoomUpdateCause { Timeline = "TIMELINE", RoomRead = "ROOM_READ", // TODO: Use this. + NewRoom = "NEW_ROOM", } diff --git a/src/theme.js b/src/theme.js index 72b6e93443..ccb753d601 100644 --- a/src/theme.js +++ b/src/theme.js @@ -19,114 +19,8 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; -import dis from "./dispatcher/dispatcher"; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; -import ThemeController from "./settings/controllers/ThemeController"; - -export class ThemeWatcher { - static _instance = null; - - constructor() { - this._themeWatchRef = null; - this._systemThemeWatchRef = null; - this._dispatcherRef = null; - - // we have both here as each may either match or not match, so by having both - // we can get the tristate of dark/light/unsupported - this._preferDark = global.matchMedia("(prefers-color-scheme: dark)"); - this._preferLight = global.matchMedia("(prefers-color-scheme: light)"); - - this._currentTheme = this.getEffectiveTheme(); - } - - start() { - this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); - this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); - if (this._preferDark.addEventListener) { - this._preferDark.addEventListener('change', this._onChange); - this._preferLight.addEventListener('change', this._onChange); - } - this._dispatcherRef = dis.register(this._onAction); - } - - stop() { - if (this._preferDark.addEventListener) { - this._preferDark.removeEventListener('change', this._onChange); - this._preferLight.removeEventListener('change', this._onChange); - } - SettingsStore.unwatchSetting(this._systemThemeWatchRef); - SettingsStore.unwatchSetting(this._themeWatchRef); - dis.unregister(this._dispatcherRef); - } - - _onChange = () => { - this.recheck(); - }; - - _onAction = (payload) => { - if (payload.action === 'recheck_theme') { - // XXX forceTheme - this.recheck(payload.forceTheme); - } - }; - - // XXX: forceTheme param added here as local echo appears to be unreliable - // https://github.com/vector-im/riot-web/issues/11443 - recheck(forceTheme) { - const oldTheme = this._currentTheme; - this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; - if (oldTheme !== this._currentTheme) { - setTheme(this._currentTheme); - } - } - - getEffectiveTheme() { - // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab - - // XXX: checking the isLight flag here makes checking it in the ThemeController - // itself completely redundant since we just override the result here and we're - // now effectively just using the ThemeController as a place to store the static - // variable. The system theme setting probably ought to have an equivalent - // controller that honours the same flag, although probablt better would be to - // have the theme logic in one place rather than split between however many - // different places. - if (ThemeController.isLogin) return 'light'; - - // If the user has specifically enabled the system matching option (excluding default), - // then use that over anything else. We pick the lowest possible level for the setting - // to ensure the ordering otherwise works. - const systemThemeExplicit = SettingsStore.getValueAt( - SettingLevel.DEVICE, "use_system_theme", null, false, true); - if (systemThemeExplicit) { - console.log("returning explicit system theme"); - if (this._preferDark.matches) return 'dark'; - if (this._preferLight.matches) return 'light'; - } - - // If the user has specifically enabled the theme (without the system matching option being - // enabled specifically and excluding the default), use that theme. We pick the lowest possible - // level for the setting to ensure the ordering otherwise works. - const themeExplicit = SettingsStore.getValueAt( - SettingLevel.DEVICE, "theme", null, false, true); - if (themeExplicit) { - console.log("returning explicit theme: " + themeExplicit); - return themeExplicit; - } - - // If the user hasn't really made a preference in either direction, assume the defaults of the - // settings and use those. - if (SettingsStore.getValue('use_system_theme')) { - if (this._preferDark.matches) return 'dark'; - if (this._preferLight.matches) return 'light'; - } - console.log("returning theme value"); - return SettingsStore.getValue('theme'); - } - - isSystemThemeSupported() { - return this._preferDark.matches || this._preferLight.matches; - } -} +import SettingsStore from "./settings/SettingsStore"; +import ThemeWatcher from "./settings/watchers/ThemeWatcher"; export function enumerateThemes() { const BUILTIN_THEMES = { diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx new file mode 100644 index 0000000000..7cd59222dd --- /dev/null +++ b/src/toasts/AnalyticsToast.tsx @@ -0,0 +1,77 @@ +/* +Copyright 2020 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 dis from "../dispatcher/dispatcher"; +import Analytics from "../Analytics"; +import AccessibleButton from "../components/views/elements/AccessibleButton"; +import GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; + +const onAccept = () => { + console.log("DEBUG onAccept AnalyticsToast"); + dis.dispatch({ + action: 'accept_cookies', + }); +}; + +const onReject = () => { + console.log("DEBUG onReject AnalyticsToast"); + dis.dispatch({ + action: "reject_cookies", + }); +}; + +const onUsageDataClicked = () => { + Analytics.showDetailsModal(); +}; + +const TOAST_KEY = "analytics"; + +export const showToast = (policyUrl?: string) => { + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Help us improve Riot"), + props: { + description: _t( + "Send anonymous usage data which helps us improve Riot. " + + "This will use a cookie.", + {}, + { + "UsageDataLink": (sub) => ( + { sub } + ), + // XXX: We need to link to the page that explains our cookies + "PolicyLink": (sub) => policyUrl ? ( + { sub } + ) : sub, + }, + ), + acceptLabel: _t("I want to help"), + onAccept, + rejectLabel: _t("No"), + onReject, + }, + component: GenericToast, + priority: 10, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts new file mode 100644 index 0000000000..41717e0804 --- /dev/null +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -0,0 +1,58 @@ +/* +Copyright 2020 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 { _t } from '../languageHandler'; +import dis from "../dispatcher/dispatcher"; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import DeviceListener from '../DeviceListener'; +import GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; + +const TOAST_KEY = "reviewsessions"; + +export const showToast = (deviceIds: Set) => { + const onAccept = () => { + DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); + + dis.dispatch({ + action: 'view_user_info', + userId: MatrixClientPeg.get().getUserId(), + }); + }; + + const onReject = () => { + DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); + }; + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Review where you’re logged in"), + icon: "verification_warning", + props: { + description: _t("Verify all your sessions to ensure your account & messages are safe"), + acceptLabel: _t("Review"), + onAccept, + rejectLabel: _t("Later"), + onReject, + }, + component: GenericToast, + priority: 50, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/DesktopNotificationsToast.ts b/src/toasts/DesktopNotificationsToast.ts new file mode 100644 index 0000000000..413e82e20b --- /dev/null +++ b/src/toasts/DesktopNotificationsToast.ts @@ -0,0 +1,50 @@ +/* +Copyright 2020 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 { _t } from "../languageHandler"; +import Notifier from "../Notifier"; +import GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; + +const onAccept = () => { + Notifier.setEnabled(true); +}; + +const onReject = () => { + Notifier.setToolbarHidden(true); +}; + +const TOAST_KEY = "desktopnotifications"; + +export const showToast = () => { + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Notifications"), + props: { + description: _t("You are not receiving desktop notifications"), + acceptLabel: _t("Enable them now"), + onAccept, + rejectLabel: _t("Close"), + onReject, + }, + component: GenericToast, + priority: 30, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx new file mode 100644 index 0000000000..d35140be3d --- /dev/null +++ b/src/toasts/ServerLimitToast.tsx @@ -0,0 +1,50 @@ +/* +Copyright 2020 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, _td } from "../languageHandler"; +import GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; +import {messageForResourceLimitError} from "../utils/ErrorUtils"; + +const TOAST_KEY = "serverlimit"; + +export const showToast = (limitType: string, adminContact?: string, syncError?: boolean) => { + const errorText = messageForResourceLimitError(limitType, adminContact, { + 'monthly_active_user': _td("Your homeserver has exceeded its user limit."), + '': _td("Your homeserver has exceeded one of its resource limits."), + }); + const contactText = messageForResourceLimitError(limitType, adminContact, { + '': _td("Contact your server admin."), + }); + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Warning"), + props: { + description: {errorText} {contactText}, + acceptLabel: _t("Ok"), + onAccept: hideToast, + }, + component: GenericToast, + priority: 70, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/SetPasswordToast.ts b/src/toasts/SetPasswordToast.ts new file mode 100644 index 0000000000..88cc317978 --- /dev/null +++ b/src/toasts/SetPasswordToast.ts @@ -0,0 +1,47 @@ +/* +Copyright 2020 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 { _t } from "../languageHandler"; +import Modal from "../Modal"; +import SetPasswordDialog from "../components/views/dialogs/SetPasswordDialog"; +import GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; + +const onAccept = () => { + Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog); +}; + +const TOAST_KEY = "setpassword"; + +export const showToast = () => { + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Set password"), + props: { + description: _t("To return to your account in future you need to set a password"), + acceptLabel: _t("Set Password"), + onAccept, + rejectLabel: _t("Later"), + onReject: hideToast, // it'll return on reload + }, + component: GenericToast, + priority: 60, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts new file mode 100644 index 0000000000..d35bbf1c88 --- /dev/null +++ b/src/toasts/SetupEncryptionToast.ts @@ -0,0 +1,106 @@ +/* +Copyright 2020 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 Modal from "../Modal"; +import * as sdk from "../index"; +import { _t } from "../languageHandler"; +import DeviceListener from "../DeviceListener"; +import SetupEncryptionDialog from "../components/views/dialogs/SetupEncryptionDialog"; +import { accessSecretStorage } from "../CrossSigningManager"; +import ToastStore from "../stores/ToastStore"; +import GenericToast from "../components/views/toasts/GenericToast"; + +const TOAST_KEY = "setupencryption"; + +const getTitle = (kind: Kind) => { + switch (kind) { + case Kind.SET_UP_ENCRYPTION: + return _t("Set up encryption"); + case Kind.UPGRADE_ENCRYPTION: + return _t("Encryption upgrade available"); + case Kind.VERIFY_THIS_SESSION: + return _t("Verify this session"); + } +}; + +const getSetupCaption = (kind: Kind) => { + switch (kind) { + case Kind.SET_UP_ENCRYPTION: + return _t("Set up"); + case Kind.UPGRADE_ENCRYPTION: + return _t("Upgrade"); + case Kind.VERIFY_THIS_SESSION: + return _t("Verify"); + } +}; + +const getDescription = (kind: Kind) => { + switch (kind) { + case Kind.SET_UP_ENCRYPTION: + case Kind.UPGRADE_ENCRYPTION: + return _t("Verify yourself & others to keep your chats safe"); + case Kind.VERIFY_THIS_SESSION: + return _t("Other users may not trust it"); + } +}; + +export enum Kind { + SET_UP_ENCRYPTION = "set_up_encryption", + UPGRADE_ENCRYPTION = "upgrade_encryption", + VERIFY_THIS_SESSION = "verify_this_session", +} + +const onReject = () => { + DeviceListener.sharedInstance().dismissEncryptionSetup(); +}; + +export const showToast = (kind: Kind) => { + const onAccept = async () => { + if (kind === Kind.VERIFY_THIS_SESSION) { + Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, + {}, null, /* priority = */ false, /* static = */ true); + } else { + const Spinner = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog( + Spinner, null, "mx_Dialog_spinner", /* priority */ false, /* static */ true, + ); + try { + await accessSecretStorage(); + } finally { + modal.close(); + } + } + }; + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: getTitle(kind), + icon: "verification_warning", + props: { + description: getDescription(kind), + acceptLabel: getSetupCaption(kind), + onAccept, + rejectLabel: _t("Later"), + onReject, + }, + component: GenericToast, + priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts new file mode 100644 index 0000000000..9dedd2b137 --- /dev/null +++ b/src/toasts/UnverifiedSessionToast.ts @@ -0,0 +1,70 @@ +/* +Copyright 2020 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 { _t } from '../languageHandler'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import Modal from '../Modal'; +import DeviceListener from '../DeviceListener'; +import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog'; +import ToastStore from "../stores/ToastStore"; +import GenericToast from "../components/views/toasts/GenericToast"; + +function toastKey(deviceId: string) { + return "unverified_session_" + deviceId; +} + +export const showToast = (deviceId: string) => { + const cli = MatrixClientPeg.get(); + + const onAccept = () => { + Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, { + userId: cli.getUserId(), + device: cli.getStoredDevice(cli.getUserId(), deviceId), + onFinished: (r) => { + if (!r) { + /* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */ + DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); + } + }, + }, null, /* priority = */ false, /* static = */ true); + }; + + const onReject = () => { + DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); + }; + + const device = cli.getStoredDevice(cli.getUserId(), deviceId); + + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey(deviceId), + title: _t("New login. Was this you?"), + icon: "verification_warning", + props: { + description: _t( + "Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}), + acceptLabel: _t("Verify"), + onAccept, + rejectLabel: _t("Later"), + onReject, + }, + component: GenericToast, + priority: 80, + }); +}; + +export const hideToast = (deviceId: string) => { + ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); +}; diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx new file mode 100644 index 0000000000..7a8d3671db --- /dev/null +++ b/src/toasts/UpdateToast.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2020 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 GenericToast from "../components/views/toasts/GenericToast"; +import ToastStore from "../stores/ToastStore"; +import QuestionDialog from "../components/views/dialogs/QuestionDialog"; +import ChangelogDialog from "../components/views/dialogs/ChangelogDialog"; +import PlatformPeg from "../PlatformPeg"; +import Modal from "../Modal"; + +const TOAST_KEY = "update"; + +/* + * Check a version string is compatible with the Changelog + * dialog ([riot-version]-react-[react-sdk-version]-js-[js-sdk-version]) + */ +function checkVersion(ver) { + const parts = ver.split('-'); + return parts.length === 5 && parts[1] === 'react' && parts[3] === 'js'; +} + +function installUpdate() { + PlatformPeg.get().installUpdate(); +} + +export const showToast = (version: string, newVersion: string, releaseNotes?: string) => { + function onReject() { + PlatformPeg.get().deferUpdate(newVersion); + } + + let onAccept; + let acceptLabel = _t("What's new?"); + if (releaseNotes) { + onAccept = () => { + Modal.createTrackedDialog('Display release notes', '', QuestionDialog, { + title: _t("What's New"), + description:
    {releaseNotes}
    , + button: _t("Update"), + onFinished: (update) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get().installUpdate(); + } + }, + }); + }; + } else if (checkVersion(version) && checkVersion(newVersion)) { + onAccept = () => { + Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, { + version, + newVersion, + onFinished: (update) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get().installUpdate(); + } + }, + }); + }; + } else { + onAccept = installUpdate; + acceptLabel = _t("Restart"); + } + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Upgrade your Riot"), + props: { + description: _t("A new version of Riot is available!"), + acceptLabel, + onAccept, + rejectLabel: _t("Later"), + onReject, + }, + component: GenericToast, + priority: 20, + }); +}; + +export const hideToast = () => { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +}; diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index ac7ac8c9ec..6558a11ed4 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -31,7 +31,7 @@ export function isContentActionable(mxEvent) { // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; - if (isSent) { + if (isSent && !mxEvent.isRedacted()) { if (mxEvent.getType() === 'm.room.message') { const content = mxEvent.getContent(); if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { diff --git a/src/utils/IDisposable.ts b/src/utils/IDisposable.ts new file mode 100644 index 0000000000..bf03e3bc85 --- /dev/null +++ b/src/utils/IDisposable.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 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. +*/ + +export interface IDisposable { + dispose(): void; +} diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index 35ec1a0269..d65bc4bd07 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -29,11 +29,6 @@ export default class ResizeNotifier extends EventEmitter { this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); } - notifyBannersChanged() { - this.emit("leftPanelResized"); - this.emit("middlePanelResized"); - } - // can be called in quick succession notifyLeftHandleResized() { // don't emit event for own region diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts new file mode 100644 index 0000000000..3a84990b56 --- /dev/null +++ b/src/utils/arrays.ts @@ -0,0 +1,47 @@ +/* +Copyright 2020 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. +*/ + +/** + * Determines if two arrays are different through a shallow comparison. + * @param a The first array. Must be defined. + * @param b The second array. Must be defined. + * @returns True if they are the same, false otherwise. + */ +export function arrayHasDiff(a: any[], b: any[]): boolean { + if (a.length === b.length) { + // When the lengths are equal, check to see if either array is missing + // an element from the other. + if (b.some(i => !a.includes(i))) return true; + if (a.some(i => !b.includes(i))) return true; + } else { + return true; // different lengths means they are naturally diverged + } +} + +/** + * Performs a diff on two arrays. The result is what is different with the + * first array (`added` in the returned object means objects in B that aren't + * in A). Shallow comparisons are used to perform the diff. + * @param a The first array. Must be defined. + * @param b The second array. Must be defined. + * @returns The diff between the arrays. + */ +export function arrayDiff(a: T[], b: T[]): { added: T[], removed: T[] } { + return { + added: b.filter(i => !a.includes(i)), + removed: a.filter(i => !b.includes(i)), + }; +} diff --git a/src/utils/read-receipts.ts b/src/utils/read-receipts.ts new file mode 100644 index 0000000000..f05c3cc5f2 --- /dev/null +++ b/src/utils/read-receipts.ts @@ -0,0 +1,34 @@ +/* +Copyright 2020 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +/** + * Determines if a read receipt update event includes the client's own user. + * @param event The event to check. + * @param client The client to check against. + * @returns True if the read receipt update includes the client, false otherwise. + */ +export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient): boolean { + const myUserId = client.getUserId(); + for (const eventId of Object.keys(event.getContent())) { + const receiptUsers = Object.keys(event.getContent()[eventId]['m.read'] || {}); + if (receiptUsers.includes(myUserId)) { + return true; + } + } +} diff --git a/src/verification.js b/src/verification.js index 289ac9544b..1dccb7dc28 100644 --- a/src/verification.js +++ b/src/verification.js @@ -22,12 +22,11 @@ import { _t } from './languageHandler'; import {RIGHT_PANEL_PHASES} from "./stores/RightPanelStorePhases"; import {findDMForUser} from './createRoom'; import {accessSecretStorage} from './CrossSigningManager'; -import SettingsStore from './settings/SettingsStore'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; async function enable4SIfNeeded() { const cli = MatrixClientPeg.get(); - if (!cli.isCryptoEnabled() || !SettingsStore.getValue("feature_cross_signing")) { + if (!cli.isCryptoEnabled()) { return false; } const usk = cli.getCrossSigningId("user_signing"); diff --git a/src/widgets/Jitsi.ts b/src/widgets/Jitsi.ts index 15df4953aa..a52f8182aa 100644 --- a/src/widgets/Jitsi.ts +++ b/src/widgets/Jitsi.ts @@ -16,10 +16,8 @@ limitations under the License. import SdkConfig from "../SdkConfig"; import {MatrixClientPeg} from "../MatrixClientPeg"; -import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery"; const JITSI_WK_PROPERTY = "im.vector.riot.jitsi"; -const JITSI_WK_CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours, arbitrarily selected export interface JitsiWidgetData { conferenceId: string; @@ -36,39 +34,27 @@ export class Jitsi { return this.domain || 'jitsi.riot.im'; } - constructor() { - // We rely on the first call to be an .update() instead of doing one here. Doing one - // here could result in duplicate calls to the homeserver. - - // Start a timer to update the server info regularly - setInterval(() => this.update(), JITSI_WK_CHECK_INTERVAL); + public start() { + const cli = MatrixClientPeg.get(); + cli.on("WellKnown.client", this.update); + // call update initially in case we missed the first WellKnown.client event and for if no well-known present + this.update(cli.getClientWellKnown()); } - public async update(): Promise { + private update = async (discoveryResponse): Promise => { // Start with a default of the config's domain let domain = (SdkConfig.get()['jitsi'] || {})['preferredDomain'] || 'jitsi.riot.im'; - // Now request the .well-known config to see if it changed - if (MatrixClientPeg.get()) { - try { - console.log("Attempting to get Jitsi conference information from homeserver"); - - const homeserverDomain = MatrixClientPeg.getHomeserverName(); - const discoveryResponse = await AutoDiscovery.getRawClientConfig(homeserverDomain); - if (discoveryResponse && discoveryResponse[JITSI_WK_PROPERTY]) { - const wkPreferredDomain = discoveryResponse[JITSI_WK_PROPERTY]['preferredDomain']; - if (wkPreferredDomain) domain = wkPreferredDomain; - } - } catch (e) { - // These are non-fatal errors - console.error(e); - } + console.log("Attempting to get Jitsi conference information from homeserver"); + if (discoveryResponse && discoveryResponse[JITSI_WK_PROPERTY]) { + const wkPreferredDomain = discoveryResponse[JITSI_WK_PROPERTY]['preferredDomain']; + if (wkPreferredDomain) domain = wkPreferredDomain; } // Put the result into memory for us to use later this.domain = domain; console.log("Jitsi conference domain:", this.preferredDomain); - } + }; /** * Parses the given URL into the data needed for a Jitsi widget, if the widget diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 212afac5c4..07cd51edbd 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -205,9 +205,8 @@ describe("", () => { expect(content.html()).toBe('' + 'Hey ' + '' + - 'Member' + + 'Member' + ''); }); }); diff --git a/test/end-to-end-tests/src/scenario.js b/test/end-to-end-tests/src/scenario.js index f575fb392e..2191d630ac 100644 --- a/test/end-to-end-tests/src/scenario.js +++ b/test/end-to-end-tests/src/scenario.js @@ -17,6 +17,7 @@ limitations under the License. const {range} = require('./util'); const signup = require('./usecases/signup'); +const toastScenarios = require('./scenarios/toast'); const roomDirectoryScenarios = require('./scenarios/directory'); const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); @@ -37,6 +38,7 @@ module.exports = async function scenario(createSession, restCreator) { const alice = await createUser("alice"); const bob = await createUser("bob"); + await toastScenarios(alice, bob); await roomDirectoryScenarios(alice, bob); await e2eEncryptionScenarios(alice, bob); console.log("create REST users:"); diff --git a/test/end-to-end-tests/src/scenarios/toast.js b/test/end-to-end-tests/src/scenarios/toast.js new file mode 100644 index 0000000000..1206ef40b0 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/toast.js @@ -0,0 +1,49 @@ +/* +Copyright 2020 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. +*/ + +const {assertNoToasts, acceptToast, rejectToast} = require("../usecases/toasts"); + +module.exports = async function toastScenarios(alice, bob) { + console.log(" checking and clearing toasts:"); + + alice.log.startGroup(`clears toasts`); + alice.log.step(`reject desktop notifications toast`); + await rejectToast(alice, "Notifications"); + alice.log.done(); + + alice.log.step(`accepts analytics toast`); + await acceptToast(alice, "Help us improve Riot"); + alice.log.done(); + + alice.log.step(`checks no remaining toasts`); + await assertNoToasts(alice); + alice.log.done(); + alice.log.endGroup(); + + bob.log.startGroup(`clears toasts`); + bob.log.step(`reject desktop notifications toast`); + await rejectToast(bob, "Notifications"); + bob.log.done(); + + bob.log.step(`reject analytics toast`); + await rejectToast(bob, "Help us improve Riot"); + bob.log.done(); + + bob.log.step(`checks no remaining toasts`); + await assertNoToasts(bob); + bob.log.done(); + bob.log.endGroup(); +}; diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index 55c2ed440c..907ee2fb8e 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -122,8 +122,8 @@ module.exports = class RiotSession { await input.type(text); } - query(selector, timeout = DEFAULT_TIMEOUT) { - return this.page.waitForSelector(selector, {visible: true, timeout}); + query(selector, timeout = DEFAULT_TIMEOUT, hidden = false) { + return this.page.waitForSelector(selector, {visible: true, timeout, hidden}); } async queryAll(selector) { diff --git a/test/end-to-end-tests/src/usecases/dialog.js b/test/end-to-end-tests/src/usecases/dialog.js index d4ae97dff9..15ac50bb18 100644 --- a/test/end-to-end-tests/src/usecases/dialog.js +++ b/test/end-to-end-tests/src/usecases/dialog.js @@ -20,7 +20,7 @@ const assert = require('assert'); async function assertDialog(session, expectedTitle) { const titleElement = await session.query(".mx_Dialog .mx_Dialog_title"); const dialogHeader = await session.innerText(titleElement); - assert(dialogHeader, expectedTitle); + assert.equal(dialogHeader, expectedTitle); } async function acceptDialog(session, expectedTitle) { diff --git a/test/end-to-end-tests/src/usecases/toasts.js b/test/end-to-end-tests/src/usecases/toasts.js new file mode 100644 index 0000000000..db78352f2b --- /dev/null +++ b/test/end-to-end-tests/src/usecases/toasts.js @@ -0,0 +1,47 @@ +/* +Copyright 2020 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. +*/ + +const assert = require('assert'); + +async function assertNoToasts(session) { + try { + await session.query('.mx_Toast_toast', 1000, true); + } catch (e) { + const h2Element = await session.query('.mx_Toast_title h2', 1000); + const toastTitle = await session.innerText(h2Element); + throw new Error(`"${toastTitle}" toast found when none expected`); + } +} + +async function assertToast(session, expectedTitle) { + const h2Element = await session.query('.mx_Toast_title h2'); + const toastTitle = await session.innerText(h2Element); + assert.equal(toastTitle, expectedTitle); +} + +async function acceptToast(session, expectedTitle) { + await assertToast(session, expectedTitle); + const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary'); + await btn.click(); +} + +async function rejectToast(session, expectedTitle) { + await assertToast(session, expectedTitle); + const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger'); + await btn.click(); +} + +module.exports = {assertNoToasts, assertToast, acceptToast, rejectToast}; diff --git a/test/skinned-sdk.js b/test/skinned-sdk.js index bc13d78815..876a188cc0 100644 --- a/test/skinned-sdk.js +++ b/test/skinned-sdk.js @@ -16,7 +16,6 @@ const components = {}; components['structures.LeftPanel'] = stubComponent(); components['structures.RightPanel'] = stubComponent(); components['structures.RoomDirectory'] = stubComponent(); -components['views.globals.MatrixToolbar'] = stubComponent(); components['views.globals.GuestWarningBar'] = stubComponent(); components['views.globals.NewVersionBar'] = stubComponent(); components['views.elements.Spinner'] = stubComponent({displayName: 'Spinner'}); diff --git a/yarn.lock b/yarn.lock index d08979f5e9..7e444a0e0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,28 +25,28 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== +"@babel/compat-data@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.6.tgz#3f604c40e420131affe6f2c8052e9a275ae2049b" + integrity sha512-5QPTrNen2bm7RBc7dsOmcA5hbrS4O2Vhmk5XOL4zWW/zD/hV0iinpefDlkm+tBBy8kDtFaaeEvmAqt+nURAV2g== dependencies: - browserslist "^4.9.1" + browserslist "^4.11.1" invariant "^2.2.4" semver "^5.5.0" "@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376" + integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" + "@babel/generator" "^7.9.6" "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" + "@babel/helpers" "^7.9.6" + "@babel/parser" "^7.9.6" "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/traverse" "^7.9.6" + "@babel/types" "^7.9.6" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -56,12 +56,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.8.3", "@babel/generator@^7.9.0", "@babel/generator@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" - integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.8.3", "@babel/generator@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43" + integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ== dependencies: - "@babel/types" "^7.9.5" + "@babel/types" "^7.9.6" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -98,27 +98,27 @@ "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/types" "^7.9.0" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== +"@babel/helper-compilation-targets@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.9.6.tgz#1e05b7ccc9d38d2f8b40b458b380a04dcfadd38a" + integrity sha512-x2Nvu0igO0ejXzx09B/1fGBxY9NXQlBW2kZsSxCJft+KHN8t9XWzIvFxtPHnBOAXpVsdxZKZFbRUC8TsNKajMw== dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" + "@babel/compat-data" "^7.9.6" + browserslist "^4.11.1" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.5.tgz#79753d44017806b481017f24b02fd4113c7106ea" - integrity sha512-IipaxGaQmW4TfWoXdqjY0TzoXQ1HRS0kPpEgvjosb3u7Uedcq297xFqDQiCcQtRRwzIMif+N1MLVI8C5a4/PAA== +"@babel/helper-create-class-features-plugin@^7.8.3", "@babel/helper-create-class-features-plugin@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.6.tgz#965c8b0a9f051801fd9d3b372ca0ccf200a90897" + integrity sha512-6N9IeuyHvMBRyjNYOMJHrhwtu4WJMrYf8hVbEHD3pbbbmNOk1kmXSQs7bA4dYDUaIx4ZEzdnvo6NwC3WHd/Qow== dependencies: "@babel/helper-function-name" "^7.9.5" "@babel/helper-member-expression-to-functions" "^7.8.3" "@babel/helper-optimise-call-expression" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" + "@babel/helper-replace-supers" "^7.9.6" "@babel/helper-split-export-declaration" "^7.8.3" "@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": @@ -227,15 +227,15 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== +"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6", "@babel/helper-replace-supers@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz#03149d7e6a5586ab6764996cd31d6981a17e1444" + integrity sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA== dependencies: "@babel/helper-member-expression-to-functions" "^7.8.3" "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/traverse" "^7.9.6" + "@babel/types" "^7.9.6" "@babel/helper-simple-access@^7.8.3": version "7.8.3" @@ -267,14 +267,14 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== +"@babel/helpers@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.6.tgz#092c774743471d0bb6c7de3ad465ab3d3486d580" + integrity sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw== dependencies: "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/traverse" "^7.9.6" + "@babel/types" "^7.9.6" "@babel/highlight@^7.8.3": version "7.9.0" @@ -285,10 +285,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.0", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.0", "@babel/parser@^7.8.6", "@babel/parser@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7" + integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q== "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" @@ -356,10 +356,10 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-numeric-separator" "^7.8.3" -"@babel/plugin-proposal-object-rest-spread@^7.7.4", "@babel/plugin-proposal-object-rest-spread@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz#3fd65911306d8746014ec0d0cf78f0e39a149116" - integrity sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg== +"@babel/plugin-proposal-object-rest-spread@^7.7.4", "@babel/plugin-proposal-object-rest-spread@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.6.tgz#7a093586fcb18b08266eb1a7177da671ac575b63" + integrity sha512-Ga6/fhGqA9Hj+y6whNpPv8psyaK5xzrQwSPsGPloVkvmH+PqW1ixdnfJ9uIO06OjQNYol3PMnfmJ8vfZtkzF+A== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" @@ -615,34 +615,34 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== +"@babel/plugin-transform-modules-amd@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.6.tgz#8539ec42c153d12ea3836e0e3ac30d5aae7b258e" + integrity sha512-zoT0kgC3EixAyIAU+9vfaUVKTv9IxBDSabgHoUCBP6FqEJ+iNiN7ip7NBKcYqbfUDfuC2mFCbM7vbu4qJgOnDw== dependencies: "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== +"@babel/plugin-transform-modules-commonjs@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz#64b7474a4279ee588cacd1906695ca721687c277" + integrity sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ== dependencies: "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== +"@babel/plugin-transform-modules-systemjs@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.6.tgz#207f1461c78a231d5337a92140e52422510d81a4" + integrity sha512-NW5XQuW3N2tTHim8e1b7qGy7s0kZ2OH3m5octc49K1SdAKGxYxeIx7hiIz05kS1R2R+hOWcsr1eYwcGhrdHsrg== dependencies: "@babel/helper-hoist-variables" "^7.8.3" "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.9.0": version "7.9.0" @@ -746,9 +746,9 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-transform-runtime@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.0.tgz#45468c0ae74cc13204e1d3b1f4ce6ee83258af0b" - integrity sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.6.tgz#3ba804438ad0d880a17bca5eaa0cdf1edeedb2fd" + integrity sha512-qcmiECD0mYOjOIt8YHNsAP1SxPooC/rDmfmiSK9BNY72EitdSc7l44WTEklaWuFtbOEBjNhWWyph/kOImbNJ4w== dependencies: "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" @@ -793,11 +793,11 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-transform-typescript@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.9.4.tgz#4bb4dde4f10bbf2d787fce9707fb09b483e33359" - integrity sha512-yeWeUkKx2auDbSxRe8MusAG+n4m9BFY/v+lPjmQDgOFX5qnySkUY5oXzkp6FwPdsYqnKay6lorXYdC0n3bZO7w== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.9.6.tgz#2248971416a506fc78278fc0c0ea3179224af1e9" + integrity sha512-8OvsRdvpt3Iesf2qsAn+YdlwAJD7zJ+vhFZmDCa4b8dTp7MmHtKk5FF2mCsGxjZwuwsy/yIIay/nLmxST1ctVQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.9.6" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-typescript" "^7.8.3" @@ -810,12 +810,12 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/preset-env@^7.7.6": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.5.tgz#8ddc76039bc45b774b19e2fc548f6807d8a8919f" - integrity sha512-eWGYeADTlPJH+wq1F0wNfPbVS1w1wtmMJiYk55Td5Yu28AsdR9AsC97sZ0Qq8fHqQuslVSIYSGJMcblr345GfQ== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.6.tgz#df063b276c6455ec6fcfc6e53aacc38da9b0aea6" + integrity sha512-0gQJ9RTzO0heXOhzftog+a/WyOuqMrAIugVYxMYf83gh1CQaQDjMtsOpqOwXyDL/5JcWsrCm8l4ju8QC97O7EQ== dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" + "@babel/compat-data" "^7.9.6" + "@babel/helper-compilation-targets" "^7.9.6" "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-proposal-async-generator-functions" "^7.8.3" @@ -823,7 +823,7 @@ "@babel/plugin-proposal-json-strings" "^7.8.3" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.5" + "@babel/plugin-proposal-object-rest-spread" "^7.9.6" "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" "@babel/plugin-proposal-optional-chaining" "^7.9.0" "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" @@ -850,9 +850,9 @@ "@babel/plugin-transform-function-name" "^7.8.3" "@babel/plugin-transform-literals" "^7.8.3" "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" + "@babel/plugin-transform-modules-amd" "^7.9.6" + "@babel/plugin-transform-modules-commonjs" "^7.9.6" + "@babel/plugin-transform-modules-systemjs" "^7.9.6" "@babel/plugin-transform-modules-umd" "^7.9.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" "@babel/plugin-transform-new-target" "^7.8.3" @@ -868,8 +868,8 @@ "@babel/plugin-transform-typeof-symbol" "^7.8.4" "@babel/plugin-transform-unicode-regex" "^7.8.3" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.5" - browserslist "^4.9.1" + "@babel/types" "^7.9.6" + browserslist "^4.11.1" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" @@ -926,17 +926,17 @@ source-map-support "^0.5.16" "@babel/runtime-corejs3@^7.8.3": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7" - integrity sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.9.6.tgz#67aded13fffbbc2cb93247388cf84d77a4be9a71" + integrity sha512-6toWAfaALQjt3KMZQc6fABqZwUDDuWzz+cAfPhqyEnzxvdWOAkjwPNxgF8xlmo7OWLsSjaKjsskpKHRLaMArOA== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" + integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== dependencies: regenerator-runtime "^0.13.4" @@ -949,25 +949,25 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" - integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442" + integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.5" + "@babel/generator" "^7.9.6" "@babel/helper-function-name" "^7.9.5" "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.5" + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" - integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" + integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== dependencies: "@babel/helper-validator-identifier" "^7.9.5" lodash "^4.17.13" @@ -1142,12 +1142,14 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@peculiar/asn1-schema@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-1.0.5.tgz#aa5a2c51225d213d1d6a5499ada926da3f556ff5" - integrity sha512-rzzorGYnQNmVHleLvC8gJSbbdNYtg+EB9s075dHvwpxs10teXHYnRmTWhCVuWjbSVSofwdm7IYPtMTWTbcNUWA== +"@peculiar/asn1-schema@^2.0.1", "@peculiar/asn1-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.3.tgz#c6c097e842ebb8a07d198b68cd49f2cf9f3571b5" + integrity sha512-STqC+Tfx2dTiIGRmokjsKOeqsfhoV6WaBwFr7BVicSfHLAVSPrZXiugyD8AELrjQdJ9INWpL3N7YSJyU5a1ZwA== dependencies: + "@types/asn1js" "^0.0.1" asn1js "^2.0.26" + pvtsutils "^1.0.10" tslib "^1.11.1" "@peculiar/json-schema@^1.1.10": @@ -1158,23 +1160,30 @@ tslib "^1.11.1" "@peculiar/webcrypto@^1.0.22": - version "1.0.27" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.0.27.tgz#f32c58656267c8f8419a6b574322573a1b83a683" - integrity sha512-sERMakD19gNhwBVXGGoJjBfc28bDbd2YWaio7/x8jKtvwMKNuljM7ANQ6LzEkEvqFAyjf3bhBZktJ6UXy/0Plg== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.1.tgz#4c7498e4861878e299ef058bce1208a4d063d0ff" + integrity sha512-Bu2XgOvzirnLcojZYs4KQ8hOLf2ETpa0NL6btQt5NgsAwctI6yVkzgYP+EcG7Mm579RBP+V0LM5rXyMlTVx23A== dependencies: - "@peculiar/asn1-schema" "^1.0.5" + "@peculiar/asn1-schema" "^2.0.3" "@peculiar/json-schema" "^1.1.10" pvtsutils "^1.0.10" - tslib "^1.11.1" - webcrypto-core "^1.0.19-next.0" + tslib "^1.11.2" + webcrypto-core "^1.1.0" "@sinonjs/commons@^1.7.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" - integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== dependencies: type-detect "4.0.8" +"@types/asn1js@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-0.0.1.tgz#ef8b9f9708cb1632a1c3a9cd27717caabe793bc2" + integrity sha1-74uflwjLFjKhw6nNJ3F8qr55O8I= + dependencies: + "@types/pvutils" "*" + "@types/babel__core@^7.1.0": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -1202,9 +1211,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.10" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.10.tgz#d9a99f017317d9b3d1abc2ced45d3bca68df0daf" - integrity sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.11.tgz#1ae3010e8bf8851d324878b42acec71986486d18" + integrity sha512-ddHK5icION5U6q11+tV2f9Mo6CZVuT8GJKld2q9LqHSZbvLbH34Kcu2yFGckZut453+eQU6btIA3RihmnRgI+Q== dependencies: "@babel/types" "^7.3.0" @@ -1241,9 +1250,9 @@ "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" - integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.2.tgz#79d7a78bad4219f4c03d6557a1c72d9ca6ba62d5" + integrity sha512-rsZg7eL+Xcxsxk2XlBt9KcG8nOp9iYdKCOikY9x2RFJCyOdNj4MKPQty0e8oZr29vVAzKXr1BmR+kZauti3o1w== "@types/istanbul-lib-report@*": version "3.0.0" @@ -1253,9 +1262,9 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" - integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" + integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" @@ -1265,6 +1274,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/lodash@^4.14.152": + version "4.14.152" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" + integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1276,15 +1290,25 @@ integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA== "@types/node@*": - version "13.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== + version "14.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" + integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== + +"@types/node@^12.12.41": + version "12.12.42" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.42.tgz#d0d1149336bd07540dd1ea576692829d575dec34" + integrity sha512-R/9QdYFLL9dE9l5cWWzWIZByVGFd7lk7JVOJ7KD+E1SJ4gni7XJRLz9QTjyYQiHIqEAgku9VgxdLjMlhhUaAFg== "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/pvutils@*": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@types/pvutils/-/pvutils-0.0.2.tgz#e21684962cfa58ac920fd576d90556032dc86009" + integrity sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w== + "@types/qrcode@^1.3.4": version "1.3.4" resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.4.tgz#984d97bb72caa558d470158701081ccb712f616b" @@ -1292,7 +1316,14 @@ dependencies: "@types/node" "*" -"@types/react@*": +"@types/react-dom@^16.9.8": + version "16.9.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" + integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.9": version "16.9.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== @@ -1300,14 +1331,6 @@ "@types/prop-types" "*" csstype "^2.2.0" -"@types/react@16.9": - version "16.9.32" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383" - integrity sha512-fmejdp0CTH00mOJmxUPPbWCEBWPvRIL4m8r0qD+BSDUqmutPyGQCHifzMpMzdvZwROdEdL78IuZItntFWgPXHQ== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1340,9 +1363,9 @@ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^13.0.0": - version "13.0.8" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.8.tgz#a38c22def2f1c2068f8971acb3ea734eb3c64a99" - integrity sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA== + version "13.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.9.tgz#44028e974343c7afcf3960f1a2b1099c39a7b5e1" + integrity sha512-xrvhZ4DZewMDhoH1utLtOAwYQy60eYFoXeje30TzM3VOvQlBwQaEpKFq5m34k1wOw2AKIi2pwtiAjdmhvlBUzg== dependencies: "@types/yargs-parser" "*" @@ -1352,26 +1375,26 @@ integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg== "@typescript-eslint/experimental-utils@^2.5.0": - version "2.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a" - integrity sha512-vOsYzjwJlY6E0NJRXPTeCGqjv5OHgRU1kzxHKWJVPjDYGbPgLudBXjIlc+OD1hDBZ4l1DLbOc5VjofKahsu9Jw== + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" + integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.27.0" + "@typescript-eslint/typescript-estree" "2.34.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/typescript-estree@2.27.0": - version "2.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.27.0.tgz#a288e54605412da8b81f1660b56c8b2e42966ce8" - integrity sha512-t2miCCJIb/FU8yArjAvxllxbTiyNqaXJag7UOpB5DVoM3+xnjeOngtqlJkLRnMtzaRcJhe3CIR9RmL40omubhg== +"@typescript-eslint/typescript-estree@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" + integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" glob "^7.1.6" is-glob "^4.0.1" lodash "^4.17.15" - semver "^6.3.0" + semver "^7.3.2" tsutils "^3.17.1" "@webassemblyjs/ast@1.9.0": @@ -1557,7 +1580,7 @@ acorn@^5.5.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1: +acorn@^6.0.1, acorn@^6.0.7, acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== @@ -1610,9 +1633,9 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: - version "6.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" - integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1709,7 +1732,7 @@ array-find-index@^1.0.1: resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= -array-includes@^3.0.3, array-includes@^3.1.1: +array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== @@ -1835,17 +1858,17 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^9.0.0: - version "9.7.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4" - integrity sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ== + version "9.8.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.0.tgz#68e2d2bef7ba4c3a65436f662d0a56a741e56511" + integrity sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A== dependencies: - browserslist "^4.11.1" - caniuse-lite "^1.0.30001039" + browserslist "^4.12.0" + caniuse-lite "^1.0.30001061" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.27" - postcss-value-parser "^4.0.3" + postcss "^7.0.30" + postcss-value-parser "^4.1.0" await-lock@^2.0.1: version "2.0.1" @@ -1858,9 +1881,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" - integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + version "1.10.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" + integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== babel-eslint@^10.0.3: version "10.1.0" @@ -1887,10 +1910,10 @@ babel-jest@^24.9.0: chalk "^2.4.2" slash "^2.0.0" -babel-plugin-dynamic-import-node@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== dependencies: object.assign "^4.1.0" @@ -1997,14 +2020,19 @@ bluebird@^3.5.0, bluebird@^3.5.5: integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== blueimp-canvas-to-blob@^3.5.0: - version "3.18.0" - resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.18.0.tgz#15f67cd1469f0be4d90c4619a0499a76bb835f79" - integrity sha512-AkYW5KQ0kTMrmcXvSVi+2TsWDXVZwrJM3g4o7r2z6OA3IlMhlAnoBNWI1ow45jfRr/co7tNch4OdNyb3WU3Pxw== + version "3.27.0" + resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e" + integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" + integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== boolbase@~1.0.0: version "1.0.0" @@ -2100,7 +2128,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0: +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= @@ -2109,17 +2137,19 @@ browserify-rsa@^4.0.0: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + version "4.2.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" + integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.2" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" browserify-zlib@^0.2.0: version "0.2.0" @@ -2128,13 +2158,13 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.11.1, browserslist@^4.8.3, browserslist@^4.9.1: - version "4.11.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.1.tgz#92f855ee88d6e050e7e7311d987992014f1a1f1b" - integrity sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g== +browserslist@^4.11.1, browserslist@^4.12.0, browserslist@^4.8.5: + version "4.12.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" + integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== dependencies: - caniuse-lite "^1.0.30001038" - electron-to-chromium "^1.3.390" + caniuse-lite "^1.0.30001043" + electron-to-chromium "^1.3.413" node-releases "^1.1.53" pkg-up "^2.0.0" @@ -2190,9 +2220,9 @@ buffer@^4.3.0: isarray "^1.0.0" buffer@^5.4.3: - version "5.5.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" - integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" @@ -2291,10 +2321,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001038, caniuse-lite@^1.0.30001039: - version "1.0.30001039" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001039.tgz#b3814a1c38ffeb23567f8323500c09526a577bbe" - integrity sha512-SezbWCTT34eyFoWHgx8UWso7YtvtM7oosmFoXbCkdC6qJzRfBTeTgE9REtKtiuKXuMwWTZEvdnFNGAyVMorv8Q== +caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061: + version "1.0.30001065" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001065.tgz#e8d7fef61cdfd8a7107493ad6bf551a4eb59c68f" + integrity sha512-DDxCLgJ266YnAHQv0jS1wdOaihRFF52Zgmlag39sQJVy2H46oROpJp4hITstqhdB8qnHSrKNoAEkQA9L/oYF9A== capture-exit@^2.0.0: version "2.0.0" @@ -2378,10 +2408,10 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== +chokidar@^3.3.1, chokidar@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" + integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -2389,7 +2419,7 @@ chokidar@^3.3.1: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.3.0" + readdirp "~3.4.0" optionalDependencies: fsevents "~2.1.2" @@ -2428,7 +2458,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.1.2: +classnames@^2.1.2, classnames@^2.2.5: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -2441,9 +2471,9 @@ cli-cursor@^2.1.0: restore-cursor "^2.0.0" cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" - integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== cliui@^4.0.0: version "4.1.0" @@ -2618,17 +2648,17 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.6.2: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" - integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" + integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== dependencies: - browserslist "^4.8.3" + browserslist "^4.8.5" semver "7.0.0" core-js-pure@^3.0.0: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a" - integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== core-js@^1.0.0: version "1.2.7" @@ -2679,7 +2709,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-hash@^1.1.0, create-hash@^1.1.2: +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -2690,7 +2720,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -2941,9 +2971,9 @@ diff-dom@^4.1.3: updates "^8.5.2" diff-match-patch@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" - integrity sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== diff-sequences@^24.9.0: version "24.9.0" @@ -3064,9 +3094,9 @@ domutils@^1.5.1: domelementtype "1" domutils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.0.0.tgz#15b8278e37bfa8468d157478c58c367718133c08" - integrity sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.1.0.tgz#7ade3201af43703fde154952e3a868eb4b635f16" + integrity sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg== dependencies: dom-serializer "^0.2.1" domelementtype "^2.0.1" @@ -3097,12 +3127,12 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.390: - version "1.3.398" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.398.tgz#4c01e29091bf39e578ac3f66c1f157d92fa5725d" - integrity sha512-BJjxuWLKFbM5axH3vES7HKMQgAknq9PZHBkMK/rEXUQG9i1Iw5R+6hGkm6GtsQSANjSUrh/a6m32nzCNDNo/+w== +electron-to-chromium@^1.3.413: + version "1.3.451" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.451.tgz#0c075af3e2f06d706670bde0279432802ca8c83f" + integrity sha512-2fvco0F2bBIgqzO8GRP0Jt/91pdrf9KfZ5FsmkYkjERmIJG585cFeFZV4+CO6oTmU3HmCTgfcZuEa7kW8VUh3A== -elliptic@^6.0.0: +elliptic@^6.0.0, elliptic@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== @@ -3125,15 +3155,15 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojibase-data@^4.0.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-4.2.1.tgz#3d1f0c69ddbb2ca7b7014f5e34654190802a40df" - integrity sha512-O0vxoPMgVkRq/uII/gdAjz9RwNv6ClJrd3J9QCCRC4btZRmeut/qohC/Fi+NNXUcjY08RWNTvxSnq/vij8hvrw== +emojibase-data@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-5.0.1.tgz#ce6fe36b4affd3578e0be8779211018a2fdae960" + integrity sha512-rYWlogJ2q5P78U8Xx1vhsXHcYKu1wFnr7+o6z9QHssZ1SsJLTCkJINZIPHRFWuDreAUK457TkqHpdOXElu0fzA== -emojibase-regex@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-3.2.1.tgz#122935958c9a49c96bb29ac69ccbbac0b2e7022d" - integrity sha512-VAX2Rc2U/alu5q6P2cET2alzC63o1Uarm6Ea/b3ab+KOzxZT4JKmB0tCU1sTZvfNKa16KMLCK2k7hJBHJq4vWQ== +emojibase-regex@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-4.0.1.tgz#a2cd4bbb42825422da9ec72f15e970bc2c90b46a" + integrity sha512-S42UHkFfz15i4NNz+wi9iMKFo+B6Kalc6PJLpYX0BUANViXw4vSyYZMFdBGRLduSabWHuEcTLZl9xOa2YP3eJw== emojis-list@^2.0.0: version "2.1.0" @@ -3183,9 +3213,9 @@ entities@^1.1.1, "entities@~ 1.1.1", entities@~1.1.1: integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + version "2.0.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.2.tgz#ac74db0bba8d33808bbf36809c3a5c3683531436" + integrity sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw== enzyme-adapter-react-16@^1.15.1: version "1.15.2" @@ -3362,9 +3392,9 @@ eslint-plugin-flowtype@^2.30.0: lodash "^4.17.10" eslint-plugin-jest@^23.0.4: - version "23.8.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4" - integrity sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg== + version "23.13.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.13.1.tgz#b2ce83f76064ad8ba1f1f26f322b86a86e44148e" + integrity sha512-TRLJH6M6EDvGocD98a7yVThrAOCK9WJfo9phuUb0MJptcrOYZeCKzC9aOzZCD93sxXCsiJVZywaTHdI/mAi0FQ== dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" @@ -3374,9 +3404,9 @@ eslint-plugin-react-hooks@^2.0.1: integrity sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g== eslint-plugin-react@^7.7.0: - version "7.19.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666" - integrity sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ== + version "7.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz#f98712f0a5e57dfd3e5542ef0604b8739cd47be3" + integrity sha512-rqe1abd0vxMjmbPngo4NaYxTcR3Y4Hrmc/jg4T+sYz63yqlmJRknpEQfmWY+eDWPuMmix6iUIK+mv0zExjeLgA== dependencies: array-includes "^3.1.1" doctrine "^2.1.0" @@ -3387,7 +3417,6 @@ eslint-plugin-react@^7.7.0: object.values "^1.1.1" prop-types "^15.7.2" resolve "^1.15.1" - semver "^6.3.0" string.prototype.matchall "^4.0.2" xregexp "^4.3.0" @@ -3488,11 +3517,11 @@ esprima@^4.0.0, esprima@^4.0.1: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe" - integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q== + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== dependencies: - estraverse "^5.0.0" + estraverse "^5.1.0" esrecurse@^4.1.0: version "4.2.1" @@ -3506,10 +3535,10 @@ estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" - integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== estree-walker@^0.5.0: version "0.5.2" @@ -3880,10 +3909,10 @@ flux@2.1.1: fbjs "0.1.0-alpha.7" immutable "^3.7.4" -focus-lock@^0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7" - integrity sha512-Dx69IXGCq1qsUExWuG+5wkiMqVM/zGx/reXSJSLogECwp3x6KeNQZ+NAetgxEFpnC41rD8U3+jRCW68+LNzdtw== +focus-lock@^0.6.7: + version "0.6.8" + resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.8.tgz#61985fadfa92f02f2ee1d90bc738efaf7f3c9f46" + integrity sha512-vkHTluRCoq9FcsrldC0ulQHiyBYgVJB2CX53I8r0nTC6KnEij7Of0jpBspjt3/CuNb6fyoj3aOh9J2HgQUM0og== focus-visible@^5.0.2: version "5.1.0" @@ -3950,17 +3979,17 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== dependencies: bindings "^1.5.0" nan "^2.12.1" fsevents@~2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== function-bind@^1.1.1: version "1.1.1" @@ -4151,9 +4180,9 @@ gonzales-pe@^4.2.3: minimist "^1.2.5" graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== growly@^1.3.0: version "1.3.0" @@ -4227,12 +4256,13 @@ has@^1.0.1, has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" @@ -4297,9 +4327,9 @@ html-encoding-sniffer@^1.0.2: whatwg-encoding "^1.0.1" html-entities@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== html-escaper@^2.0.0: version "2.0.2" @@ -4398,9 +4428,9 @@ ignore@^4.0.3, ignore@^4.0.6: integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== ignore@^5.0.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + version "5.1.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.6.tgz#643194ad4bf2712f37852e386b6998eff0db2106" + integrity sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA== immutable@^3.7.4: version "3.8.2" @@ -4469,7 +4499,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4819,11 +4849,6 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" @@ -5356,9 +5381,9 @@ jest@^24.9.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -5460,11 +5485,11 @@ jsprim@^1.2.2: verror "1.10.0" jsx-ast-utils@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" - integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.3.0.tgz#edd727794ea284d7fda575015ed1b0cde0289ab6" + integrity sha512-3HNoc7nZ1hpZIKB3hJ7BlFRkzCx2BynRtfSwbkqZdpRdvAPsGMnzclPwrvDBS7/lalHTj21NwIeaEpysHBOudg== dependencies: - array-includes "^3.0.3" + array-includes "^3.1.1" object.assign "^4.1.0" kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: @@ -5662,9 +5687,9 @@ log-symbols@^2.0.0, log-symbols@^2.2.0: chalk "^2.0.1" loglevel@^1.6.4: - version "1.6.7" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" - integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== + version "1.6.8" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" + integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== lolex@^5.1.2: version "5.1.2" @@ -5777,8 +5802,8 @@ mathml-tag-names@^2.0.1: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "6.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a4a7097c103da42075f2c70e070fd01fa6fb0d48" + version "6.2.1" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ebe66bdd6e0f6edbc60be1612c5a1fc0c9ea092c" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" @@ -5906,17 +5931,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0: - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "1.43.0" + mime-db "1.44.0" mimic-fn@^1.0.0: version "1.2.0" @@ -6027,9 +6052,9 @@ mute-stream@0.0.7: integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= nan@^2.12.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== nanomatch@^1.2.9: version "1.2.13" @@ -6054,9 +6079,9 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nearley@^2.7.10: - version "2.19.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.1.tgz#4af4006e16645ff800e9f993c3af039857d9dbdc" - integrity sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg== + version "2.19.3" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.3.tgz#ae3b040e27616b5348102c436d1719209476a5a1" + integrity sha512-FpAy1PmTsUpOtgxr23g4jRNvJHYzZEW2PixXeSzksLR/ykPfwKhAodc2+9wQhY+JneWLcvkDw6q7FJIsIdF/aQ== dependencies: commander "^2.19.0" moo "^0.5.0" @@ -6142,9 +6167,9 @@ node-notifier@^5.4.2: which "^1.3.0" node-releases@^1.1.53: - version "1.1.53" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" - integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== + version "1.1.56" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.56.tgz#bc054a417d316e3adac90eafb7e1932802f28705" + integrity sha512-EVo605FhWLygH8a64TjgpjyHYOihkxECwX1bHHr8tETJKWEiWS2YJjPbvsX2jFjnjTNEgBCmk9mLjKG1Mf11cw== normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.5.0" @@ -6232,9 +6257,12 @@ object-inspect@^1.1.0, object-inspect@^1.7.0: integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== object-is@^1.0.1, object-is@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" - integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" + integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.9, object-keys@^1.1.1: version "1.1.1" @@ -6259,13 +6287,12 @@ object.assign@^4.1.0: object-keys "^1.0.11" object.entries@^1.1.0, object.entries@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" - integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== dependencies: define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.17.5" has "^1.0.3" object.fromentries@^2.0.2: @@ -6441,7 +6468,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0: +parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== @@ -6575,7 +6602,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.7: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -6703,11 +6730,11 @@ postcss-sass@^0.3.5: postcss "^7.0.1" postcss-scss@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.0.0.tgz#248b0a28af77ea7b32b1011aba0f738bda27dea1" - integrity sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug== + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" + integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== dependencies: - postcss "^7.0.0" + postcss "^7.0.6" postcss-selector-parser@^3.1.0: version "3.1.2" @@ -6737,15 +6764,15 @@ postcss-value-parser@^3.3.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" - integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.7: - version "7.0.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== +postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.6, postcss@^7.0.7: + version "7.0.30" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2" + integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -6831,7 +6858,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6928,9 +6955,9 @@ qrcode@^1.4.4: yargs "^13.2.4" qs@^6.5.2, qs@^6.6.0: - version "6.9.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" - integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== qs@~6.5.2: version "6.5.2" @@ -7035,13 +7062,21 @@ react-dom@^16.9.0: prop-types "^15.6.2" scheduler "^0.19.1" +react-draggable@^4.0.3: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.2.tgz#f3cefecee25f467f865144cda0d066e5f05f94a0" + integrity sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + react-focus-lock@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.2.1.tgz#1d12887416925dc53481914b7cedd39494a3b24a" - integrity sha512-47g0xYcCTZccdzKRGufepY8oZ3W1Qg+2hn6u9SHZ0zUB6uz/4K4xJe7yYFNZ1qT6m+2JDm82F6QgKeBTbjW4PQ== + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47" + integrity sha512-j15cWLPzH0gOmRrUg01C09Peu8qbcdVqr6Bjyfxj80cNZmH+idk/bNBYEDSmkAtwkXI+xEYWSmHYqtaQhZ8iUQ== dependencies: "@babel/runtime" "^7.0.0" - focus-lock "^0.6.6" + focus-lock "^0.6.7" prop-types "^15.6.2" react-clientside-effect "^1.2.2" use-callback-ref "^1.2.1" @@ -7079,6 +7114,14 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-resizable@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4" + integrity sha512-Jd/bKOKx6+19NwC4/aMLRu/J9/krfxlDnElP41Oc+oLiUWs/zwV1S9yBfBZRnqAwQb6vQ/HRSk3bsSWGSgVbpw== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" @@ -7145,7 +7188,7 @@ read-pkg@^4.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: +readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -7163,12 +7206,12 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== dependencies: - picomatch "^2.0.7" + picomatch "^2.2.1" realpath-native@^1.1.0: version "1.1.0" @@ -7277,9 +7320,9 @@ registry-auth-token@4.0.0: safe-buffer "^5.0.1" regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== regjsparser@^0.6.4: version "0.6.4" @@ -7461,9 +7504,9 @@ resolve@1.1.7: integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= resolve@^1.10.0, resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.8.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" @@ -7539,11 +7582,9 @@ rsvp@^4.8.4: integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== run-async@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" - integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== - dependencies: - is-promise "^2.1.0" + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" @@ -7559,10 +7600,10 @@ rxjs@^6.4.0, rxjs@^6.5.2: dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" @@ -7597,9 +7638,9 @@ sane@^4.0.3: walker "~1.0.5" sanitize-html@^1.18.4: - version "1.22.1" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.1.tgz#5b36c92ab27917ddd2775396815c2bc1a6268310" - integrity sha512-++IMC00KfMQc45UWZJlhWOlS9eMrME38sFG9GXfR+k6oBo9JXSYQgTOZCl9j3v/smFTRNT9XNwz5DseFdMY+2Q== + version "1.24.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.24.0.tgz#9cd42f236512bfcf6259424e958551148c165a7f" + integrity sha512-TAIFx39V/y06jDd4YUz7ntCdMUXN5Z28pSG7sTP2BCLXwHA9+ermacDpQs35Evo4p6YSgmaPdSbGiX4Fgptuuw== dependencies: chalk "^2.4.1" htmlparser2 "^4.1.0" @@ -7639,7 +7680,7 @@ schema-utils@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0, semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: +semver@6.3.0, semver@^6.0.0, semver@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -7649,6 +7690,11 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" @@ -7807,9 +7853,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -7835,22 +7881,22 @@ spawn-command@^0.0.2-1: integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" @@ -8034,9 +8080,9 @@ string.prototype.trim@^1.2.1: function-bind "^1.1.1" string.prototype.trimend@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz#ee497fd29768646d84be2c9b819e292439614373" - integrity sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== dependencies: define-properties "^1.1.3" es-abstract "^1.17.5" @@ -8060,9 +8106,9 @@ string.prototype.trimright@^2.1.1: string.prototype.trimend "^1.0.0" string.prototype.trimstart@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz#afe596a7ce9de905496919406c9734845f01a2f2" - integrity sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w== + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== dependencies: define-properties "^1.1.3" es-abstract "^1.17.5" @@ -8150,15 +8196,15 @@ stylelint-config-standard@^18.2.0: stylelint-config-recommended "^2.2.0" stylelint-scss@^3.9.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.16.0.tgz#6928fe57bcfc924110d09847c1f720472a9b7bd6" - integrity sha512-dAWs/gagdPYO3VDdvgRv5drRBMcWI4E//z3AXPAY1qYkSdXCEVJtEW+R9JtinG0U2rcJIu5XWaVddPQeaaufzw== + version "3.17.2" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.17.2.tgz#4d849a153f9241834396f5880db2c3c964def4e3" + integrity sha512-e0dmxqsofy/HZj4urcGSJw4S6yHDJxiQdT20/1ciCsd5lomisa7YM4+Qtt1EG4hsqEG1dbEeF855tec1UyqcSA== dependencies: lodash "^4.17.15" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" + postcss-value-parser "^4.1.0" stylelint@^9.10.1: version "9.10.1" @@ -8287,9 +8333,9 @@ terser-webpack-plugin@^1.4.3: worker-farm "^1.7.0" terser@^4.1.2, terser@^4.6.2: - version "4.6.10" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.10.tgz#90f5bd069ff456ddbc9503b18e52f9c493d3b7c2" - integrity sha512-qbF/3UOo11Hggsbsqm2hPa6+L4w7bkr+09FNseEe8xrcVD3APGLFqE+Oz1ZKAxjYnFsj80rLOfgAtJ0LNJjtTA== + version "4.7.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006" + integrity sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -8439,10 +8485,10 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== +tslib@^1.10.0, tslib@^1.11.1, tslib@^1.11.2, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== tslint@^5.20.1: version "5.20.1" @@ -8512,9 +8558,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^3.7.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + version "3.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" + integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== ua-parser-js@^0.7.18: version "0.7.21" @@ -8530,9 +8576,9 @@ unherit@^1.0.4: xtend "^4.0.0" unhomoglyph@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.5.tgz#a68c6244f0ec140bfe58293a1f66a9bd2a244343" - integrity sha512-rNAw2rGogjq4BVhsCX8K6qXrCcHmUaMCHETlUG0ujGZ3OHwnzJHwdMyzy3n/c9Y7lvlbckOd9nkW33grUVE3bg== + version "1.0.6" + resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3" + integrity sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg== unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" @@ -8695,9 +8741,9 @@ url@^0.11.0: querystring "0.2.0" use-callback-ref@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb" - integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w== + version "1.2.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.3.tgz#9f939dfb5740807bbf9dd79cdd4e99d27e827756" + integrity sha512-DPBPh1i2adCZoIArRlTuKRy7yue7QogtEnfv0AKrWsY+GA+4EKe37zhRDouNnyWMoNQFYZZRF+2dLHsWE4YvJA== use-sidecar@^1.0.1: version "1.0.2" @@ -8829,25 +8875,34 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -watchpack@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" - integrity sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA== +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== dependencies: chokidar "^2.1.8" + +watchpack@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" + integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== + dependencies: graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.0" + watchpack-chokidar2 "^2.0.0" -webcrypto-core@^1.0.19-next.0: - version "1.0.19" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.0.19.tgz#521a0b082afecd914b8e968efb5dc381b8416c6f" - integrity sha512-6XHExtfMJrpkFDh9MiJ/y7ptX0dfZi0ogxFyelqxMu1eFowxivHfIp6DKzT+ZjU66xTuNfJkfkUk1bIB3tEOgA== +webcrypto-core@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.1.tgz#c9cd26f8dea696d7b5f5c1b0598ff16e6bdcab7c" + integrity sha512-xK61sFRUyZdSAJG7+bJox36+Tnhxw1PaMbmrLLp30HNTJ4mffqsY2jUMlmGq6OOoej3WO/SsH5serzlzBMZ+jg== dependencies: - "@peculiar/asn1-schema" "^1.0.5" + "@peculiar/asn1-schema" "^2.0.1" "@peculiar/json-schema" "^1.1.10" asn1js "^2.0.26" pvtsutils "^1.0.10" - tslib "^1.11.1" + tslib "^1.11.2" webidl-conversions@^4.0.2: version "4.0.2" @@ -8880,15 +8935,15 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-map "~0.6.1" webpack@^4.20.2: - version "4.42.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" - integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== + version "4.43.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" + integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.2.1" + acorn "^6.4.1" ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" @@ -8905,13 +8960,13 @@ webpack@^4.20.2: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.0" + watchpack "^1.6.1" webpack-sources "^1.4.1" what-input@^5.2.6: - version "5.2.7" - resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.7.tgz#81afbb6b82882cff8c43fa7ff1054aa46f288ffa" - integrity sha512-ruCP2skyygi0ZHnMicHuZP7vXnJh8uJXs9R7RX488HlWigSdzngdmKo5Ti11Iatp1dnYp55VfioP/WevPaK+xQ== + version "5.2.9" + resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.9.tgz#e484628c00404d2ad5d747ac2f0fb22008f7757a" + integrity sha512-/tuM/4ngvfYB1QF3yekJsmFpIhkiHEDKCl/VYDikyHZVxoFn3U/lNgiNt7aqC8RerkoPUMxc9ihKsW9KwAx2Rg== whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.5"