diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 9973cfb120..db12611ade 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -6,7 +6,6 @@ src/components/structures/RoomView.js src/components/structures/ScrollPanel.js src/components/structures/SearchBox.js src/components/structures/UploadBar.js -src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/SetPasswordDialog.js @@ -15,9 +14,7 @@ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0706e20085..e5515f1015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,284 @@ +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) + + * Fix key backup restore with SSSS + [\#4617](https://github.com/matrix-org/matrix-react-sdk/pull/4617) + * Remove SSSS key upgrade check from rageshake + [\#4616](https://github.com/matrix-org/matrix-react-sdk/pull/4616) + +Changes in [2.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0) (2020-05-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0-rc.1...v2.6.0) + + * Upgrade to JS SDK 6.1.0 + * Revert "ImageView make clicking off it easier" + [\#4602](https://github.com/matrix-org/matrix-react-sdk/pull/4602) + * Remove debugging that causes email addresses to load forever (to release) + [\#4598](https://github.com/matrix-org/matrix-react-sdk/pull/4598) + +Changes in [2.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0-rc.1) (2020-05-14) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0...v2.6.0-rc.1) + + * Upgrade to JS SDK 6.1.0-rc.1 + * Update from Weblate + [\#4596](https://github.com/matrix-org/matrix-react-sdk/pull/4596) + * Fix message edits dialog being wrong and sometimes crashing + [\#4595](https://github.com/matrix-org/matrix-react-sdk/pull/4595) + * Acquire a new session before enacting deactivation + [\#4584](https://github.com/matrix-org/matrix-react-sdk/pull/4584) + * Remove UI for upgrading 4S to symmetric encryption + [\#4581](https://github.com/matrix-org/matrix-react-sdk/pull/4581) + * Add copy to SSO prompts during cross-signing setup + [\#4555](https://github.com/matrix-org/matrix-react-sdk/pull/4555) + * Re-fix OpenID requests from widgets + [\#4592](https://github.com/matrix-org/matrix-react-sdk/pull/4592) + * Fix persistent widgets on desktop / http + [\#4591](https://github.com/matrix-org/matrix-react-sdk/pull/4591) + * Updated link and added:Yarn two is not yet used. + [\#4589](https://github.com/matrix-org/matrix-react-sdk/pull/4589) + * Fix topic dialog not supporting escape as it didn't have a "Close" + [\#4578](https://github.com/matrix-org/matrix-react-sdk/pull/4578) + * Default to public room when creating room from room directory + [\#4579](https://github.com/matrix-org/matrix-react-sdk/pull/4579) + * Replace png flags and add Kosovo to country code dropdown + [\#4576](https://github.com/matrix-org/matrix-react-sdk/pull/4576) + * Rename `trash (custom).svg` as electron doesn't like paths with spaces + [\#4583](https://github.com/matrix-org/matrix-react-sdk/pull/4583) + * Fix sign in / up links on previewed rooms + [\#4582](https://github.com/matrix-org/matrix-react-sdk/pull/4582) + * Avoid soft crash if unknown device in verification + [\#4580](https://github.com/matrix-org/matrix-react-sdk/pull/4580) + * Add slash commands /query and /msg to match IRC + [\#4568](https://github.com/matrix-org/matrix-react-sdk/pull/4568) + * Send cross-signing debug booleans over rageshake + [\#4570](https://github.com/matrix-org/matrix-react-sdk/pull/4570) + * Prompt user to specify an alternate server if theirs has registration off + [\#4575](https://github.com/matrix-org/matrix-react-sdk/pull/4575) + * Don't try and redact redactions for "Remove recent messages" + [\#4573](https://github.com/matrix-org/matrix-react-sdk/pull/4573) + * View Source should target the replacing event rather than the root one + [\#4571](https://github.com/matrix-org/matrix-react-sdk/pull/4571) + * Fix passphrase reset in key backup restore dialog + [\#4569](https://github.com/matrix-org/matrix-react-sdk/pull/4569) + * Ensure key backup gets dealt with correctly during secret storage reset + [\#4556](https://github.com/matrix-org/matrix-react-sdk/pull/4556) + * Fix crash for broken invites + [\#4565](https://github.com/matrix-org/matrix-react-sdk/pull/4565) + * Fix rageshake with no matrix client + [\#4572](https://github.com/matrix-org/matrix-react-sdk/pull/4572) + * Update from Weblate + [\#4567](https://github.com/matrix-org/matrix-react-sdk/pull/4567) + * Bring back UnknownBody for UISIs + [\#4564](https://github.com/matrix-org/matrix-react-sdk/pull/4564) + * clear tag panel selection if the community selected is left + [\#4559](https://github.com/matrix-org/matrix-react-sdk/pull/4559) + * Close ImageView when redacting + [\#4560](https://github.com/matrix-org/matrix-react-sdk/pull/4560) + * Redesign redactions + [\#4484](https://github.com/matrix-org/matrix-react-sdk/pull/4484) + * Don't try to reload profile information when closing the user panel + [\#4547](https://github.com/matrix-org/matrix-react-sdk/pull/4547) + * Fix right panel hiding when viewing room member + [\#4558](https://github.com/matrix-org/matrix-react-sdk/pull/4558) + * Don't erase password confirm on registration error + [\#4540](https://github.com/matrix-org/matrix-react-sdk/pull/4540) + * Add a loading state for email addresses/phone numbers in settings + [\#4557](https://github.com/matrix-org/matrix-react-sdk/pull/4557) + * set the meta tag for theme-color to the same theme css background + [\#4554](https://github.com/matrix-org/matrix-react-sdk/pull/4554) + * Update Invite Dialog copy to include email addresses + [\#4497](https://github.com/matrix-org/matrix-react-sdk/pull/4497) + * Fix slider toggle regression. + [\#4546](https://github.com/matrix-org/matrix-react-sdk/pull/4546) + * Fix a crash where a name could unexpectedly be an empty list + [\#4552](https://github.com/matrix-org/matrix-react-sdk/pull/4552) + * Solves communities can be dragged from context menu + [\#4492](https://github.com/matrix-org/matrix-react-sdk/pull/4492) + * Remove prefixes for composer avatar urls + [\#4553](https://github.com/matrix-org/matrix-react-sdk/pull/4553) + * Fix reply RR spacing getting doubled + [\#4541](https://github.com/matrix-org/matrix-react-sdk/pull/4541) + * Differentiate copy for own untrusted device dialog + [\#4549](https://github.com/matrix-org/matrix-react-sdk/pull/4549) + * EventIndex: Reduce the logging the event index is producing. + [\#4548](https://github.com/matrix-org/matrix-react-sdk/pull/4548) + * Increase rageshake size limit to 5mb + [\#4543](https://github.com/matrix-org/matrix-react-sdk/pull/4543) + * Update from Weblate + [\#4542](https://github.com/matrix-org/matrix-react-sdk/pull/4542) + * Guard against race when waiting for cross-signing to be ready + [\#4539](https://github.com/matrix-org/matrix-react-sdk/pull/4539) + * Wait for user to be verified in e2e setup + [\#4537](https://github.com/matrix-org/matrix-react-sdk/pull/4537) + * Convert MatrixChat to a TypeScript class + [\#4462](https://github.com/matrix-org/matrix-react-sdk/pull/4462) + * Mark room as read when escape is pressed + [\#4271](https://github.com/matrix-org/matrix-react-sdk/pull/4271) + * Only show key backup reminder when confirmed by server to be missing + [\#4534](https://github.com/matrix-org/matrix-react-sdk/pull/4534) + * Add device name to unverified session toast + [\#4535](https://github.com/matrix-org/matrix-react-sdk/pull/4535) + * Show progress when loading keys + [\#4507](https://github.com/matrix-org/matrix-react-sdk/pull/4507) + * Fix device verification toasts not disappearing + [\#4532](https://github.com/matrix-org/matrix-react-sdk/pull/4532) + * Update toast copy again + [\#4529](https://github.com/matrix-org/matrix-react-sdk/pull/4529) + * Re-apply theme after login + [\#4518](https://github.com/matrix-org/matrix-react-sdk/pull/4518) + * Reduce maximum width of toasts & allow multiple lines + [\#4525](https://github.com/matrix-org/matrix-react-sdk/pull/4525) + * Treat sessions that are there when we log in as old + [\#4524](https://github.com/matrix-org/matrix-react-sdk/pull/4524) + * Allow resetting storage from the access dialog + [\#4521](https://github.com/matrix-org/matrix-react-sdk/pull/4521) + * Update (bulk) unverified device toast copy + [\#4522](https://github.com/matrix-org/matrix-react-sdk/pull/4522) + * Make new device toasts appear above review toasts + [\#4519](https://github.com/matrix-org/matrix-react-sdk/pull/4519) + * Separate toasts for existing & new device verification + [\#4511](https://github.com/matrix-org/matrix-react-sdk/pull/4511) + * Slightly darker toggle off bg color + [\#4477](https://github.com/matrix-org/matrix-react-sdk/pull/4477) + * Fix pill vertical align + [\#4514](https://github.com/matrix-org/matrix-react-sdk/pull/4514) + * Fix set up encryption toast to use "set up" as action + [\#4502](https://github.com/matrix-org/matrix-react-sdk/pull/4502) + * Don't enable e2ee when inviting a 3pid + [\#4509](https://github.com/matrix-org/matrix-react-sdk/pull/4509) + * Fix internal link styling in Security Settings + [\#4510](https://github.com/matrix-org/matrix-react-sdk/pull/4510) + * Small custom theming fixes + [\#4508](https://github.com/matrix-org/matrix-react-sdk/pull/4508) + * Fix scaling issues + [\#4355](https://github.com/matrix-org/matrix-react-sdk/pull/4355) + * Aggregate device verify toasts + [\#4506](https://github.com/matrix-org/matrix-react-sdk/pull/4506) + * Support setting username and avatar colors in custom themes + [\#4503](https://github.com/matrix-org/matrix-react-sdk/pull/4503) + * only clear on continuations where the clear isn't done by SenderProfile + [\#4501](https://github.com/matrix-org/matrix-react-sdk/pull/4501) + * cap width of editable item list item to leave space for its X button + [\#4495](https://github.com/matrix-org/matrix-react-sdk/pull/4495) + * Add a link from settings / devices to your user profile + [\#4498](https://github.com/matrix-org/matrix-react-sdk/pull/4498) + * Update from Weblate + [\#4496](https://github.com/matrix-org/matrix-react-sdk/pull/4496) + * Make icon change in SetupEncryptionDialog + [\#4485](https://github.com/matrix-org/matrix-react-sdk/pull/4485) + * Remove invite only padlocks feature flag + [\#4487](https://github.com/matrix-org/matrix-react-sdk/pull/4487) + * Fix incorrect toast if security setup skipped + [\#4486](https://github.com/matrix-org/matrix-react-sdk/pull/4486) + * Revert "Update emojibase for fixed emoji codepoints and Emoji 13 support" + [\#4482](https://github.com/matrix-org/matrix-react-sdk/pull/4482) + * Fix widget URL templating (again) + [\#4481](https://github.com/matrix-org/matrix-react-sdk/pull/4481) + * Fix recovery link on login verification flow + [\#4479](https://github.com/matrix-org/matrix-react-sdk/pull/4479) + * Make avatars in pills occupy the entire space using cropping + [\#4476](https://github.com/matrix-org/matrix-react-sdk/pull/4476) + * Use WidgetType more often to avoid breaking new sticker pickers + [\#4458](https://github.com/matrix-org/matrix-react-sdk/pull/4458) + * Update logging for unmanaged widgets, and add TODO comments for other areas + [\#4460](https://github.com/matrix-org/matrix-react-sdk/pull/4460) + * Fix OpenID requests from widgets + [\#4459](https://github.com/matrix-org/matrix-react-sdk/pull/4459) + * Take encrypted message search out of labs + [\#4467](https://github.com/matrix-org/matrix-react-sdk/pull/4467) + * Fix BigEmoji for replies + [\#4475](https://github.com/matrix-org/matrix-react-sdk/pull/4475) + * Update login security copy and design to match Figma + [\#4472](https://github.com/matrix-org/matrix-react-sdk/pull/4472) + * Fix i18n of SSO UIA copy in Deactivate Account Dialog + [\#4471](https://github.com/matrix-org/matrix-react-sdk/pull/4471) + * Assert type of domNode as HTMLElement to fix build + [\#4470](https://github.com/matrix-org/matrix-react-sdk/pull/4470) + * Unignored in settings + [\#4466](https://github.com/matrix-org/matrix-react-sdk/pull/4466) + * Skip auth flow test for signing upload when password present + [\#4464](https://github.com/matrix-org/matrix-react-sdk/pull/4464) + * If user cannot set email during registration don't tell them to + [\#4461](https://github.com/matrix-org/matrix-react-sdk/pull/4461) + * Fix post-ts autocomplete, it is not null + [\#4463](https://github.com/matrix-org/matrix-react-sdk/pull/4463) + * Convert autocomplete stuff to TypeScript + [\#4452](https://github.com/matrix-org/matrix-react-sdk/pull/4452) + * Add a back button to the devtools verifications panel + [\#4455](https://github.com/matrix-org/matrix-react-sdk/pull/4455) + * Fix: wait until cross-signing keys are fetched to show verify button + [\#4456](https://github.com/matrix-org/matrix-react-sdk/pull/4456) + * Handle load error in create secret storage dialog + [\#4451](https://github.com/matrix-org/matrix-react-sdk/pull/4451) + * Allow iframes and Jitsi URLs in /addwidget + [\#4382](https://github.com/matrix-org/matrix-react-sdk/pull/4382) + * Support m.jitsi-typed widgets as Jitsi widgets + [\#4379](https://github.com/matrix-org/matrix-react-sdk/pull/4379) + * Don't recheck DeviceListener until after initial sync is finished + [\#4450](https://github.com/matrix-org/matrix-react-sdk/pull/4450) + * Fix CSS class in ButtonPlaceholder + [\#4449](https://github.com/matrix-org/matrix-react-sdk/pull/4449) + * Password Login make sure tab takes user to password field + [\#4441](https://github.com/matrix-org/matrix-react-sdk/pull/4441) + * Network Dropdown fix things not scrolling properly + [\#4439](https://github.com/matrix-org/matrix-react-sdk/pull/4439) + * ImageView make clicking off it easier + [\#4448](https://github.com/matrix-org/matrix-react-sdk/pull/4448) + * Add slash command to send a rageshake + [\#4443](https://github.com/matrix-org/matrix-react-sdk/pull/4443) + * EventIndex: Filter out events that don't have a propper content value. + [\#4446](https://github.com/matrix-org/matrix-react-sdk/pull/4446) + * Revert "Fix Filepanel scroll position state lost when room is changed" + [\#4445](https://github.com/matrix-org/matrix-react-sdk/pull/4445) + * Update seshat copy to remove trailing full stop + [\#4442](https://github.com/matrix-org/matrix-react-sdk/pull/4442) + * Fix Filepanel scroll position state lost when room is changed + [\#4388](https://github.com/matrix-org/matrix-react-sdk/pull/4388) + * Fix end-to-end tests for end-to-end encryption verification + [\#4436](https://github.com/matrix-org/matrix-react-sdk/pull/4436) + * Don't explode if the e2e test directory exists when crashing + [\#4437](https://github.com/matrix-org/matrix-react-sdk/pull/4437) + * Bump https-proxy-agent from 2.2.1 to 2.2.4 in /test/end-to-end-tests + [\#4430](https://github.com/matrix-org/matrix-react-sdk/pull/4430) + * Minor updates to e2e test instructions on Windows + [\#4432](https://github.com/matrix-org/matrix-react-sdk/pull/4432) + * Fix typo + [\#4435](https://github.com/matrix-org/matrix-react-sdk/pull/4435) + * Catch errors sooner so users can recover more easily + [\#4122](https://github.com/matrix-org/matrix-react-sdk/pull/4122) + * Rageshake: remind user of unsupported browser and send modernizr report + [\#4381](https://github.com/matrix-org/matrix-react-sdk/pull/4381) + * Design tweaks for DM Room Tiles + [\#4338](https://github.com/matrix-org/matrix-react-sdk/pull/4338) + * Don't break spills over multiple lines, ellipsis them at max-1-line + [\#4434](https://github.com/matrix-org/matrix-react-sdk/pull/4434) + * Turn the end-to-end tests back on and fix the lazy-loading tests + [\#4433](https://github.com/matrix-org/matrix-react-sdk/pull/4433) + * Fix key backup debug panel + [\#4431](https://github.com/matrix-org/matrix-react-sdk/pull/4431) + * Convert cross-signing feature flag to setting + [\#4416](https://github.com/matrix-org/matrix-react-sdk/pull/4416) + * Make RoomPublishSetting import-skinnable + [\#4428](https://github.com/matrix-org/matrix-react-sdk/pull/4428) + * Iterate cross-signing copy + [\#4425](https://github.com/matrix-org/matrix-react-sdk/pull/4425) + * Fix: ensure twemoji font is loaded when showing SAS emojis + [\#4422](https://github.com/matrix-org/matrix-react-sdk/pull/4422) + * Revert "Fix: load Twemoji before login so complete security gets the right + emojis during SAS" + [\#4421](https://github.com/matrix-org/matrix-react-sdk/pull/4421) + * Fix: load Twemoji before login so complete security gets the right emojis + during SAS + [\#4419](https://github.com/matrix-org/matrix-react-sdk/pull/4419) + * consolidate and fix copy to clipboard + [\#4410](https://github.com/matrix-org/matrix-react-sdk/pull/4410) + * Fix Message Context Menu options not displaying: block + [\#4418](https://github.com/matrix-org/matrix-react-sdk/pull/4418) + * Fix pills being broken by unescaped characters + [\#4411](https://github.com/matrix-org/matrix-react-sdk/pull/4411) + Changes in [2.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0) (2020-05-05) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.6...v2.5.0) diff --git a/code_style.md b/code_style.md index 3ad0d38873..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 ------------- @@ -151,6 +151,7 @@ General Style Don't set things to undefined. Reserve that value to mean "not yet set to anything." Boolean objects are verboten. - Use JSDoc +- Use switch-case statements where there are 5 or more branches running against the same variable. ECMAScript ---------- diff --git a/package.json b/package.json index baf2db3381..c0e489515e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.5.0", + "version": "2.6.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -55,6 +55,7 @@ }, "dependencies": { "@babel/runtime": "^7.8.3", + "await-lock": "^2.0.1", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -117,9 +118,13 @@ "@babel/register": "^7.7.4", "@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", @@ -139,6 +144,7 @@ "flow-parser": "^0.57.3", "glob": "^5.0.14", "jest": "^24.9.0", + "jest-canvas-mock": "^2.2.0", "lolex": "^5.1.2", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", @@ -158,6 +164,7 @@ "testMatch": [ "/test/**/*-test.js" ], + "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 1faf78ebd9..eefe0f009b 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -63,7 +63,6 @@ @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"; @@ -115,7 +114,9 @@ @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; @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"; @@ -123,7 +124,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"; @@ -162,6 +162,8 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_GroupLayout.scss"; +@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @@ -200,10 +202,12 @@ @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"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; +@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.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/_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/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 4a78c8df92..1f8443e395 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -69,7 +69,7 @@ limitations under the License. height: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: $font-40px; + height: 40px; padding: 10px 0 9px 0; } @@ -116,7 +116,7 @@ limitations under the License. position: absolute; left: -15px; border-radius: 0 3px 3px 0; - top: -8px; // (16px / 2) + top: -8px; // (16px from height / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { 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/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss index 53d44e7c24..8d2e36bcd6 100644 --- a/res/css/structures/_TopLeftMenuButton.scss +++ b/res/css/structures/_TopLeftMenuButton.scss @@ -43,7 +43,7 @@ limitations under the License. margin: 0 7px; mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); mask-repeat: no-repeat; - width: 10px; + width: $font-22px; height: 6px; background-color: $roomsublist-label-fg-color; } 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/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 4d831d7858..7adcc58c4e 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -21,6 +21,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/settings.svg'); } +.mx_UserSettingsDialog_appearanceIcon::before { + mask-image: url('$(res)/img/feather-customised/brush.svg'); +} + .mx_UserSettingsDialog_voiceIcon::before { mask-image: url('$(res)/img/feather-customised/phone.svg'); } diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss new file mode 100644 index 0000000000..58ba2813b4 --- /dev/null +++ b/res/css/views/elements/_Slider.scss @@ -0,0 +1,99 @@ +/* +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_Slider { + position: relative; + margin: 0px; + flex-grow: 1; +} + +.mx_Slider_dotContainer { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.mx_Slider_bar { + display: flex; + box-sizing: border-box; + position: absolute; + height: 1em; + width: 100%; + padding: 0 0.5em; // half the width of a dot. + align-items: center; +} + +.mx_Slider_bar > hr { + width: 100%; + height: 0.4em; + background-color: $slider-background-color; + border: 0; +} + +.mx_Slider_selection { + display: flex; + align-items: center; + width: calc(100% - 1em); // 2 * half the width of a dot + height: 1em; + position: absolute; + pointer-events: none; +} + +.mx_Slider_selectionDot { + position: absolute; + width: 1.1em; + height: 1.1em; + background-color: $slider-selection-color; + border-radius: 50%; + box-shadow: 0 0 6px lightgrey; + z-index: 10; +} + +.mx_Slider_selection > hr { + margin: 0; + border: 0.2em solid $slider-selection-color; +} + +.mx_Slider_dot { + height: 1em; + width: 1em; + border-radius: 50%; + background-color: $slider-background-color; + z-index: 0; +} + +.mx_Slider_dotActive { + background-color: $slider-selection-color; +} + +.mx_Slider_dotValue { + display: flex; + flex-direction: column; + align-items: center; + color: $slider-background-color; +} + +// The following is a hack to center the labels without adding +// any width to the slider's dots. +.mx_Slider_labelContainer { + width: 1em; +} + +.mx_Slider_label { + position: relative; + width: fit-content; + left: -50%; +} 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 1b1bab67bc..e4743f189e 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -96,6 +96,10 @@ $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/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index d4e54f4473..40a80f17bb 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -37,7 +37,6 @@ limitations under the License. } .mx_EventTile_avatar { - position: absolute; top: 14px; left: 8px; cursor: pointer; @@ -68,11 +67,9 @@ limitations under the License. display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; cursor: pointer; - padding-left: 65px; /* left gutter */ padding-bottom: 0px; padding-top: 0px; margin: 0px; - line-height: $font-17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; @@ -104,9 +101,7 @@ limitations under the License. visibility: hidden; white-space: nowrap; left: 0px; - width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; - position: absolute; user-select: none; } @@ -117,10 +112,7 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; padding-left: 65px; /* left gutter */ - padding-top: 3px; - padding-bottom: 3px; border-radius: 4px; - line-height: $font-22px; } .mx_RoomView_timeline_rr_enabled, @@ -151,10 +143,6 @@ limitations under the License. margin-right: 10px; } -.mx_EventTile_info .mx_EventTile_line { - padding-left: 83px; -} - /* HACK to override line-height which is already marked important elsewhere */ .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { font-size: 48px !important; @@ -171,10 +159,15 @@ limitations under the License. } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +// The first set is to handle the 'group layout' (default) and the second for the IRC layout .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp { +.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { visibility: visible; } @@ -560,84 +553,6 @@ limitations under the License. /* end of overrides */ -.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; - } - .mx_EventTile_avatar { - top: 4px; - } - } - - .mx_EventTile .mx_SenderProfile { - font-size: $font-13px; - } - - .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.mx_EventTile_emote.mx_EventTile_continuation { - padding-top: 0; - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - } - - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - - .mx_EventTile_avatar { - top: 2px; - } - - .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_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 - } - } -} - .mx_EventTile_tileError { color: red; text-align: center; diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss new file mode 100644 index 0000000000..af12cce5d6 --- /dev/null +++ b/res/css/views/rooms/_GroupLayout.scss @@ -0,0 +1,131 @@ +/* +Copyright 2015, 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. +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. +*/ + +$left-gutter: 65px; + +.mx_GroupLayout { + + .mx_EventTile { + > .mx_SenderProfile { + line-height: $font-17px; + padding-left: $left-gutter; + } + + > .mx_EventTile_line { + padding-left: $left-gutter; + } + + > .mx_EventTile_avatar { + position: absolute; + } + + .mx_MessageTimestamp { + position: absolute; + width: 46px; /* 8 + 30 (avatar) + 8 */ + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 3px; + padding-bottom: 3px; + line-height: $font-22px; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } +} + +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + + .mx_EventTile { + padding-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_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_avatar { + top: 2px; + } + + .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; + } +} diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss new file mode 100644 index 0000000000..a8eb35eeed --- /dev/null +++ b/res/css/views/rooms/_IRCLayout.scss @@ -0,0 +1,218 @@ +/* +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. +*/ + +$icon-width: 14px; +$timestamp-width: 45px; +$right-padding: 5px; +$irc-line-height: $font-18px; + +.mx_IRCLayout { + --name-width: 70px; + + line-height: $irc-line-height !important; + + .mx_EventTile { + + // timestamps are links which shouldn't be underlined + > a { + text-decoration: none; + } + + display: flex; + flex-direction: row; + align-items: flex-start; + padding-top: 0; + + > * { + margin-right: $right-padding; + } + + > .mx_EventTile_msgOption { + order: 5; + flex-shrink: 0; + } + + > .mx_SenderProfile { + order: 2; + flex-shrink: 0; + width: var(--name-width); + text-overflow: ellipsis; + text-align: right; + display: flex; + align-items: center; + overflow: visible; + justify-content: flex-end; + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding: 0; + display: flex; + flex-direction: column; + order: 3; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + } + + > .mx_EventTile_avatar { + order: 1; + position: relative; + top: 0; + left: 0; + flex-shrink: 0; + height: $irc-line-height; + display: flex; + align-items: center; + + // Need to use important to override the js provided height and width values. + > .mx_BaseAvatar, .mx_BaseAvatar > * { + height: $font-14px !important; + width: $font-14px !important; + font-size: $font-10px !important; + line-height: $font-15px !important; + } + } + + .mx_MessageTimestamp { + font-size: $font-10px; + width: $timestamp-width; + text-align: right; + } + + > .mx_EventTile_e2eIcon { + position: relative; + right: unset; + left: unset; + padding: 0; + order: 3; + flex-shrink: 0; + flex-grow: 0; + } + + .mx_EventTile_line { + .mx_EventTile_e2eIcon, + .mx_TextualEvent, + .mx_MTextBody, + .mx_ReplyThread_wrapper_empty { + display: inline-block; + } + } + + .mx_EvenTile_line .mx_MessageActionBar, + .mx_EvenTile_line .mx_ReplyThread_wrapper { + display: block; + } + + .mx_EventTile_reply { + order: 4; + } + + .mx_EditMessageComposer_buttons { + position: relative; + } + } + + .mx_EventTile_emote { + > .mx_EventTile_avatar { + margin-left: calc(var(--name-width) + $icon-width + $right-padding); + } + } + + blockquote { + margin: 0; + } + + .mx_EventListSummary { + > .mx_EventTile_line { + padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding + } + + .mx_EventListSummary_avatars { + padding: 0; + margin: 0 9px 0 0; + } + } + + .mx_EventTile.mx_EventTile_info { + .mx_EventTile_avatar { + left: calc(var(--name-width) + 10px + $icon-width); + top: 0; + } + + .mx_EventTile_line { + left: calc(var(--name-width) + 10px + $icon-width); + } + + .mx_TextualEvent { + line-height: $irc-line-height; + } + } + + // Suppress highlight thing from the normal Layout. + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: 0; + border-left: 0; + } + + .mx_SenderProfile_hover { + background-color: $primary-bg-color; + overflow: hidden; + + > span { + display: flex; + + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .mx_SenderProfile:hover { + justify-content: flex-start; + } + + .mx_SenderProfile_hover:hover { + overflow: visible; + width: max(auto, 100%); + z-index: 10; + } + + .mx_ReplyThread { + margin: 0; + .mx_SenderProfile { + width: unset; + max-width: var(--name-width); + } + } + + .mx_ProfileResizer { + position: absolute; + height: 100%; + width: 15px; + left: calc(80px + var(--name-width)); + cursor: col-resize; + z-index: 100; + } + + // Need to use important to override the js provided height and width values. + .mx_Flair > img { + height: $font-14px !important; + width: $font-14px !important; + } +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 5bc7d5624d..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: $font-34px; + height: 34px; margin: 0; padding: 0 8px 0 10px; position: relative; @@ -81,6 +81,7 @@ limitations under the License. .mx_RoomTile_avatar_container { position: relative; + display: flex; } .mx_RoomTile_avatar { 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/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss new file mode 100644 index 0000000000..e82ae3c575 --- /dev/null +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -0,0 +1,45 @@ +/* +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_AppearanceUserSettingsTab_fontSlider, +.mx_AppearanceUserSettingsTab_themeSection .mx_Field, +.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { + @mixin mx_Settings_fullWidthField; +} + +.mx_AppearanceUserSettingsTab_fontSlider { + display: flex; + flex-direction: row; + align-items: center; + padding: 15px; + background: $font-slider-bg-color; + border-radius: 10px; + font-size: 10px; + margin-top: 24px; + margin-bottom: 24px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_smallText { + font-size: 15px; + padding-right: 20px; + padding-left: 5px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_largeText { + font-size: 18px; + padding-left: 20px; + padding-right: 5px; +} diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 95a46b51ee..6c9b89cf5a 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword .mx_Field, -.mx_GeneralUserSettingsTab_themeSection .mx_Field { +.mx_GeneralUserSettingsTab_changePassword .mx_Field { @mixin mx_Settings_fullWidthField; } diff --git a/res/img/feather-customised/brush.svg b/res/img/feather-customised/brush.svg new file mode 100644 index 0000000000..d7f2738629 --- /dev/null +++ b/res/img/feather-customised/brush.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 6224c0820f..9fb36ef1a3 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -180,6 +180,9 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: $room-highlight-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index f5f3013354..78fe2a74c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; +// Slider +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; + $progressbar-color: #000; $room-warning-bg-color: $yellow-background; @@ -302,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: rgba($input-darker-bg-color, 0.2); + // ***** Mixins! ***** @define-mixin mx_DialogButton { 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..b244993955 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,13 +15,22 @@ 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"; declare global { interface Window { Modernizr: ModernizrStatic; + mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; }; + + mx_ContentMessages: ContentMessages; + mx_ToastStore: ToastStore; + mx_DeviceListener: DeviceListener; } // 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.js b/src/BasePlatform.ts similarity index 52% rename from src/BasePlatform.js rename to src/BasePlatform.ts index 8a950dc2e3..c5f58f7f0c 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.ts @@ -1,5 +1,3 @@ -// @flow - /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd @@ -19,9 +17,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClient} from "matrix-js-sdk"; -import dis from './dispatcher'; +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 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 @@ -29,27 +41,25 @@ import BaseEventIndexManager from './indexing/BaseEventIndexManager'; * * Instances of this class are provided by the application. */ -export default class BasePlatform { - constructor() { - this.notificationCount = 0; - this.errorDidOccur = false; +export default abstract class BasePlatform { + protected notificationCount = 0; + protected errorDidOccur = false; - dis.register(this._onAction.bind(this)); + constructor() { + dis.register(this.onAction); } - _onAction(payload: Object) { + protected onAction = (payload: ActionPayload) => { switch (payload.action) { case 'on_client_not_viable': case 'on_logged_out': this.setNotificationCount(0); break; } - } + }; // Used primarily for Analytics - getHumanReadableName(): string { - return 'Base Platform'; - } + abstract getHumanReadableName(): string; setNotificationCount(count: number) { this.notificationCount = count; @@ -59,6 +69,53 @@ export default 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. @@ -84,22 +141,17 @@ export default class BasePlatform { * that is 'granted' if the user allowed the request or * 'denied' otherwise. */ - requestNotificationPermission(): Promise { - } + abstract requestNotificationPermission(): Promise; - displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { - } + abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object); loudNotification(ev: Event, room: Object) { - } + }; /** - * Returns a promise that resolves to a string representing - * the current version of the application. + * Returns a promise that resolves to a string representing the current version of the application. */ - getAppVersion(): Promise { - throw new Error("getAppVersion not implemented!"); - } + abstract getAppVersion(): Promise; /* * If it's not expected that capturing the screen will work @@ -114,20 +166,18 @@ export default class BasePlatform { * Restarts the application, without neccessarily reloading * any application code */ - reload() { - throw new Error("reload not implemented!"); - } + abstract reload(); supportsAutoLaunch(): boolean { return false; } // XXX: Surely this should be a setting like any other? - async getAutoLaunchEnabled(): boolean { + async getAutoLaunchEnabled(): Promise { return false; } - async setAutoLaunchEnabled(enabled: boolean): void { + async setAutoLaunchEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -135,11 +185,11 @@ export default class BasePlatform { return false; } - async getAutoHideMenuBarEnabled(): boolean { + async getAutoHideMenuBarEnabled(): Promise { return false; } - async setAutoHideMenuBarEnabled(enabled: boolean): void { + async setAutoHideMenuBarEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -147,11 +197,11 @@ export default class BasePlatform { return false; } - async getMinimizeToTrayEnabled(): boolean { + async getMinimizeToTrayEnabled(): Promise { return false; } - async setMinimizeToTrayEnabled(enabled: boolean): void { + async setMinimizeToTrayEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -190,4 +240,35 @@ export default class BasePlatform { 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 2bfe10850a..c95ed16eb3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -59,7 +59,7 @@ import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; diff --git a/src/ContentMessages.js b/src/ContentMessages.tsx similarity index 73% rename from src/ContentMessages.js rename to src/ContentMessages.tsx index 34379c029b..249ad8381c 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,17 +16,18 @@ 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'; +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"; @@ -39,6 +41,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 +97,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 +144,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 +174,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 +197,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 +220,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 +276,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 +302,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 +323,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 +335,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 +345,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 +369,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 +385,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 +419,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 +436,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 +485,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,11 +513,17 @@ 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'}); @@ -501,15 +531,15 @@ export default class ContentMessages { // Focus the composer view dis.dispatch({action: 'focus_composer'}); - let error; - function onProgress(ev) { upload.total = ev.total; upload.loaded = ev.loaded; 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 +550,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 +573,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 +584,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 +594,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 53% rename from src/DeviceListener.js rename to src/DeviceListener.ts index 27caba971e..ca51b5ac1c 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.ts @@ -14,43 +14,43 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClientPeg } from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import SettingsStore from './settings/SettingsStore'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import ToastStore from './stores/ToastStore'; +import { + hideToast as hideBulkUnverifiedSessionsToast, + showToast as showBulkUnverifiedSessionsToast +} from "./toasts/BulkUnverifiedSessionsToast"; +import { + hideToast as hideSetupEncryptionToast, + Kind as SetupKind, + Kind, + showToast as showSetupEncryptionToast +} from "./toasts/SetupEncryptionToast"; +import { + hideToast as hideUnverifiedSessionsToast, + showToast as showUnverifiedSessionsToast +} from "./toasts/UnverifiedSessionToast"; 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 +74,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 +87,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 +122,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,11 +163,11 @@ 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; } async _recheck() { @@ -186,48 +186,25 @@ export default class DeviceListener { const crossSigningReady = await cli.isCrossSigningReady(); - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + if (this.dismissedThisDeviceToast || crossSigningReady) { + hideSetupEncryptionToast(); } 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"), - }); - } - } + // 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(Kind.UPGRADE_ENCRYPTION); + } else { + // No cross-signing or key backup on account (set up encryption) + showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION); + } } } @@ -239,20 +216,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 +240,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/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index c9793d40f7..102afa6bf1 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -17,7 +17,7 @@ limitations under the License. */ import URL from 'url'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import {MatrixClientPeg} from "./MatrixClientPeg"; 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 1baa6c8e0c..0494628472 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -26,7 +26,7 @@ import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; import * as sdk from './index'; @@ -298,6 +298,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 +308,7 @@ async function _restoreFromLocalStorage(opts) { homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: isGuest, + pickleKey: pickleKey, }, false); return true; } else { @@ -348,9 +351,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); } /** @@ -516,7 +523,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 +584,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(); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.ts similarity index 75% rename from src/MatrixClientPeg.js rename to src/MatrixClientPeg.ts index 21f05b9759..c6ee6c546f 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,25 @@ 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; +} - // 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 +61,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 +87,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 +95,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 +172,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 +198,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. @@ -179,7 +217,7 @@ class _MatrixClientPeg { return opts; } - async start() { + public async start(): Promise { const opts = await this.assign(); console.log(`MatrixClientPeg: really starting MatrixClient`); @@ -187,7 +225,7 @@ class _MatrixClientPeg { console.log(`MatrixClientPeg: MatrixClient started`); } - getCredentials(): MatrixClientCreds { + public getCredentials(): IMatrixClientCreds { return { homeserverUrl: this.matrixClient.baseUrl, identityServerUrl: this.matrixClient.idBaseUrl, @@ -198,12 +236,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 +244,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 +263,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 +288,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/Modal.js b/src/Modal.js index de441740f1..9b9f190d58 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import Analytics from './Analytics'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import {defer} from './utils/promise'; import AsyncWrapper from './AsyncWrapper'; diff --git a/src/Notifier.js b/src/Notifier.js index ec92840998..cd328ba565 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -21,11 +21,14 @@ import PlatformPeg from './PlatformPeg'; import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; import * as Avatar from './Avatar'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; 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/Presence.js b/src/Presence.js index 2fc13a090b..42bca35f96 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -17,7 +17,7 @@ limitations under the License. */ import {MatrixClientPeg} from "./MatrixClientPeg"; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import Timer from './utils/Timer'; // Time in ms after that a user is considered as unavailable/away diff --git a/src/Registration.js b/src/Registration.js index ca162bac03..32c3d9cc35 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -20,7 +20,7 @@ limitations under the License. * registration code. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; diff --git a/src/Resend.js b/src/Resend.js index 6d6c18cf27..f5f24bffa5 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -16,7 +16,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import { EventStatus } from 'matrix-js-sdk'; export default class Resend { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 9731e42825..315c2d86f4 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -238,7 +238,7 @@ Example: import {MatrixClientPeg} from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index fbb9e2eb0e..15798ae3b1 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -21,7 +21,7 @@ limitations under the License. import * as React from 'react'; import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import {_t, _td} from './languageHandler'; import Modal from './Modal'; @@ -41,6 +41,8 @@ import { parseFragment as parseHtml } from "parse5"; import sendBugReport from "./rageshake/submit-rageshake"; import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; +import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; +import { Action } from "./dispatcher/actions"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -448,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 @@ -560,7 +562,7 @@ export const Commands = [ }), new Command({ command: 'part', - args: '[]', + args: '[]', description: _td('Leave room'), runFn: function(roomId, args) { const cli = MatrixClientPeg.get(); @@ -592,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); } } @@ -943,8 +945,10 @@ export const Commands = [ } const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); - dis.dispatch({ - action: 'view_user', + dis.dispatch({ + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the + // receiver wants. member: member || {userId}, }); return success(); diff --git a/src/UserActivity.js b/src/UserActivity.js index 0d1b4d0cc0..0174aebaf5 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import Timer from './utils/Timer'; // important these are larger than the timeouts of timers diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js deleted file mode 100644 index 006c2da5b8..0000000000 --- a/src/actions/GroupActions.js +++ /dev/null @@ -1,34 +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 { asyncAction } from './actionCreators'; - -const GroupActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to fetch - * the groups to which a user is joined. - * - * @param {MatrixClient} matrixClient the matrix client to query. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -GroupActions.fetchJoinedGroups = function(matrixClient) { - return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); -}; - -export default GroupActions; diff --git a/src/actions/GroupActions.ts b/src/actions/GroupActions.ts new file mode 100644 index 0000000000..81470d1221 --- /dev/null +++ b/src/actions/GroupActions.ts @@ -0,0 +1,34 @@ +/* +Copyright 2017 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. +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 { asyncAction } from './actionCreators'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class GroupActions { + /** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {AsyncActionPayload} An async action payload. + * @see asyncAction + */ + public static fetchJoinedGroups(matrixClient: MatrixClient): AsyncActionPayload { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups(), null); + } +} diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index c89ec44435..93a4fcf07c 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js deleted file mode 100644 index 10a3848dda..0000000000 --- a/src/actions/RoomListActions.js +++ /dev/null @@ -1,145 +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 { asyncAction } from './actionCreators'; -import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; -import Modal from '../Modal'; -import * as Rooms from '../Rooms'; -import { _t } from '../languageHandler'; -import * as sdk from '../index'; - -const RoomListActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * tag room. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {Room} room the room to tag. - * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) - * @param {string} newTag the tag with which to tag the room. - * @param {?number} oldIndex the previous position of the room in the - * list of rooms. - * @param {?number} newIndex the new position of the room in the list - * of rooms. - * @returns {function} an action thunk. - * @see asyncAction - */ -RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { - let metaData = null; - - // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); - const newList = [...lists[newTag]]; - - newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); - - // If the room was moved "down" (increasing index) in the same list we - // need to use the orders of the tiles with indices shifted by +1 - const offset = ( - newTag === oldTag && oldIndex < newIndex - ) ? 1 : 0; - - const indexBefore = offset + newIndex - 1; - const indexAfter = offset + newIndex; - - const prevOrder = indexBefore <= 0 ? - 0 : newList[indexBefore].tags[newTag].order; - const nextOrder = indexAfter >= newList.length ? - 1 : newList[indexAfter].tags[newTag].order; - - metaData = { - order: (prevOrder + nextOrder) / 2.0, - }; - } - - return asyncAction('RoomListActions.tagRoom', () => { - const promises = []; - const roomId = room.roomId; - - // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) - ) { - return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, - ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to set direct chat tag " + err); - Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { - title: _t('Failed to set direct chat tag'), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - } - - const hasChangedSubLists = oldTag !== newTag; - - // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with TAG_DM. - // - // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && - hasChangedSubLists - ) { - const promiseToDelete = matrixClient.deleteRoomTag( - roomId, oldTag, - ).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to remove tag " + oldTag + " from room: " + err); - Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { - title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - - promises.push(promiseToDelete); - } - - // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && - (hasChangedSubLists || metaData) - ) { - // metaData is the body of the PUT to set the tag, so it must - // at least be an empty object. - metaData = metaData || {}; - - const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to add tag " + newTag + " to room: " + err); - Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { - title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - - throw err; - }); - - promises.push(promiseToAdd); - } - - return Promise.all(promises); - }, () => { - // For an optimistic update - return { - room, oldTag, newTag, metaData, - }; - }); -}; - -export default RoomListActions; diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts new file mode 100644 index 0000000000..e15e1b0c65 --- /dev/null +++ b/src/actions/RoomListActions.ts @@ -0,0 +1,152 @@ +/* +Copyright 2018 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. +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 { asyncAction } from './actionCreators'; +import { TAG_DM } from '../stores/RoomListStore'; +import Modal from '../Modal'; +import * as Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import * as sdk from '../index'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; + +export default class RoomListActions { + /** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {AsyncActionPayload} an async action payload + * @see asyncAction + */ + public static tagRoom( + matrixClient: MatrixClient, room: Room, + oldTag: string, newTag: string, + oldIndex: number | null, newIndex: number | null, + ): AsyncActionPayload { + let metaData = null; + + // Is the tag ordered manually? + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const lists = RoomListStoreTempProxy.getRoomLists(); + const newList = [...lists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === TAG_DM) || + (oldTag === TAG_DM && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === TAG_DM, + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with TAG_DM. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== TAG_DM && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function (err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== TAG_DM && + (hasChangedSubLists || metaData) + ) { + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); + } +} diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js deleted file mode 100644 index a257ff16d8..0000000000 --- a/src/actions/TagOrderActions.js +++ /dev/null @@ -1,109 +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 Analytics from '../Analytics'; -import { asyncAction } from './actionCreators'; -import TagOrderStore from '../stores/TagOrderStore'; - -const TagOrderActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * move a tag in TagOrderStore to destinationIx. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to move. - * @param {number} destinationIx the new position of the tag. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { - // Only commit tags if the state is ready, i.e. not null - let tags = TagOrderStore.getOrderedTags(); - let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - if (!tags) { - return; - } - - tags = tags.filter((t) => t !== tag); - tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; - - removedTags = removedTags.filter((t) => t !== tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.moveTag', () => { - Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {tags, removedTags}; - }); -}; - -/** - * Creates an action thunk that will do an asynchronous request to - * label a tag as removed in im.vector.web.tag_ordering account data. - * - * The reason this is implemented with new state `removedTags` is that - * we incrementally and initially populate `tags` with groups that - * have been joined. If we remove a group from `tags`, it will just - * get added (as it looks like a group we've recently joined). - * - * NB: If we ever support adding of tags (which is planned), we should - * take special care to remove the tag from `removedTags` when we add - * it. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to remove. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.removeTag = function(matrixClient, tag) { - // Don't change tags, just removedTags - const tags = TagOrderStore.getOrderedTags(); - const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - - if (removedTags.includes(tag)) { - // Return a thunk that doesn't do anything, we don't even need - // an asynchronous action here, the tag is already removed. - return () => {}; - } - - removedTags.push(tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.removeTag', () => { - Analytics.trackEvent('TagOrderActions', 'removeTag'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {removedTags}; - }); -}; - -export default TagOrderActions; diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts new file mode 100644 index 0000000000..bf1820d5d1 --- /dev/null +++ b/src/actions/TagOrderActions.ts @@ -0,0 +1,111 @@ +/* +Copyright 2017 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. +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 Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class TagOrderActions { + + /** + * Creates an action thunk that will do an asynchronous request to + * move a tag in TagOrderStore to destinationIx. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to move. + * @param {number} destinationIx the new position of the tag. + * @returns {AsyncActionPayload} an async action payload that will + * dispatch actions indicating the status of the request. + * @see asyncAction + */ + public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { + // Only commit tags if the state is ready, i.e. not null + let tags = TagOrderStore.getOrderedTags(); + let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + if (!tags) { + return; + } + + tags = tags.filter((t) => t !== tag); + tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + + removedTags = removedTags.filter((t) => t !== tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.moveTag', () => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {tags, removedTags}; + }); + }; + + /** + * Creates an action thunk that will do an asynchronous request to + * label a tag as removed in im.vector.web.tag_ordering account data. + * + * The reason this is implemented with new state `removedTags` is that + * we incrementally and initially populate `tags` with groups that + * have been joined. If we remove a group from `tags`, it will just + * get added (as it looks like a group we've recently joined). + * + * NB: If we ever support adding of tags (which is planned), we should + * take special care to remove the tag from `removedTags` when we add + * it. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to remove. + * @returns {function} an async action payload that will dispatch + * actions indicating the status of the request. + * @see asyncAction + */ + public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { + // Don't change tags, just removedTags + const tags = TagOrderStore.getOrderedTags(); + const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + + if (removedTags.includes(tag)) { + // Return a thunk that doesn't do anything, we don't even need + // an asynchronous action here, the tag is already removed. + return new AsyncActionPayload(() => {}); + } + + removedTags.push(tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.removeTag', () => { + Analytics.trackEvent('TagOrderActions', 'removeTag'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {removedTags}; + }); + } +} diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.ts similarity index 76% rename from src/actions/actionCreators.js rename to src/actions/actionCreators.ts index 967ce609e7..c789e3cd07 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.ts @@ -1,5 +1,6 @@ /* Copyright 2017 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,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AsyncActionPayload } from "../dispatcher/payloads"; + /** * Create an action thunk that will dispatch actions indicating the current * status of the Promise returned by fn. @@ -25,9 +28,9 @@ limitations under the License. * @param {function?} pendingFn a function that returns an object to assign * to the `request` key of the ${id}.pending * payload. - * @returns {function} an action thunk - a function that uses its single - * argument as a dispatch function to dispatch the - * following actions: + * @returns {AsyncActionPayload} an async action payload. Includes a function + * that uses its single argument as a dispatch function + * to dispatch the following actions: * `${id}.pending` and either * `${id}.success` or * `${id}.failure`. @@ -41,12 +44,11 @@ limitations under the License. * result is the result of the promise returned by * `fn`. */ -export function asyncAction(id, fn, pendingFn) { - return (dispatch) => { +export function asyncAction(id: string, fn: () => Promise, pendingFn: () => any | null): AsyncActionPayload { + const helper = (dispatch) => { dispatch({ action: id + '.pending', - request: - typeof pendingFn === 'function' ? pendingFn() : undefined, + request: typeof pendingFn === 'function' ? pendingFn() : undefined, }); fn().then((result) => { dispatch({action: id + '.success', result}); @@ -54,4 +56,5 @@ export function asyncAction(id, fn, pendingFn) { dispatch({action: id + '.failure', err}); }); }; + return new AsyncActionPayload(helper); } 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/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js index 120b086ef6..ec4b88f759 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -17,11 +17,12 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; +import {Action} from "../../../../dispatcher/actions"; /* * Allows the user to disable the Event Index. @@ -47,7 +48,7 @@ export default class DisableEventIndexDialog extends React.Component { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } render() { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 532b2f960f..7c5170fab6 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -284,8 +284,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 +297,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 +306,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/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 9e2264a960..74552a5c08 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -19,9 +19,10 @@ import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class NewRecoveryMethodDialog extends React.PureComponent { static propTypes = { @@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = async () => { diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index c5222dafd5..cda353e717 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -18,9 +18,10 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class RecoveryMethodRemovedDialog extends React.PureComponent { static propTypes = { @@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = () => { 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/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b67e26117b..3a3cec779e 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 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/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index 6e392ea505..2753d5c4da 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -18,7 +18,7 @@ import React from 'react'; import CustomRoomTagStore from '../../stores/CustomRoomTagStore'; import AutoHideScrollbar from './AutoHideScrollbar'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import classNames from 'classnames'; import * as FormattingUtils from '../../utils/FormattingUtils'; diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 0aababf030..49ba3d1227 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -23,7 +23,7 @@ import PropTypes from 'prop-types'; import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import classnames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 3b32e5c907..a946d16319 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -21,7 +21,7 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { getHostingLink } from '../../utils/HostingLink'; import { sanitizedHtmlNode } from '../../HtmlUtils'; import { _t, _td } from '../../languageHandler'; @@ -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/HomePage.tsx b/src/components/structures/HomePage.tsx index ddf9cd6d00..ff8d35a114 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -21,7 +21,7 @@ import { getHomePageUrl } from "../../utils/pages"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a9cd12199b..a1b4f49c56 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -21,11 +21,12 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Key } from '../../Keyboard'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; +import RoomList2 from "../views/rooms/RoomList2"; const LeftPanel = createReactClass({ @@ -273,6 +274,29 @@ const LeftPanel = createReactClass({ breadcrumbs = (); } + let roomList = null; + if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { + roomList = ; + } else { + roomList = ; + } + return (
{ tagPanelContainer } @@ -284,15 +308,7 @@ const LeftPanel = createReactClass({ { exploreButton } { searchBox }
- + {roomList} ); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9de2aac8e9..1ad38c6f04 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -27,11 +27,10 @@ import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +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 RoomListStore from "../../stores/RoomListStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; @@ -42,6 +41,17 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; 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"; + // 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. @@ -56,7 +66,7 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; - onRegistered: (credentials: MatrixClientCreds) => Promise; + onRegistered: (credentials: IMatrixClientCreds) => Promise; viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; @@ -64,10 +74,6 @@ interface IProps { initialEventPixelOffset: number; leftDisabled: boolean; rightDisabled: boolean; - showCookieBar: boolean; - hasNewVersion: boolean; - userHasGeneratedPassword: boolean; - showNotifierToolbar: boolean; page_type: string; autoJoin: boolean; thirdPartyInvite?: object; @@ -75,7 +81,6 @@ interface IProps { currentRoomId: string; ConferenceHandler?: object; collapseLhs: boolean; - checkingForUpdate: boolean; config: { piwik: { policyUrl: string; @@ -85,10 +90,8 @@ interface IProps { currentUserId?: string; currentGroupId?: string; currentGroupIsNew?: boolean; - version?: string; - newVersion?: string; - newVersionReleaseNotes?: string; } + interface IState { mouseDown?: { x: number; @@ -96,8 +99,6 @@ interface IState { }; syncErrorData: any; useCompactLayout: boolean; - serverNoticeEvents: MatrixEvent[]; - userHasGeneratedPassword: boolean; } /** @@ -140,11 +141,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 @@ -178,18 +176,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); @@ -219,9 +205,11 @@ class LoggedInView extends React.PureComponent { }; _setStateFromSessionStore = () => { - this.setState({ - userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), - }); + if (this._sessionStore.getCachedPassword()) { + showSetPasswordToast(); + } else { + hideSetPasswordToast(); + } }; _createResizer() { @@ -293,22 +281,37 @@ class LoggedInView extends React.PureComponent { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { this._updateServerNoticeEvents(); + } else { + this._calculateServerLimitToast(data); } }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStore.getRoomLists(); - if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; - _updateServerNoticeEvents = async () => { - const roomLists = RoomListStore.getRoomLists(); - if (!roomLists['m.server_notice']) return []; + _calculateServerLimitToast(syncErrorData, usageLimitEventContent?) { + const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + if (error) { + usageLimitEventContent = syncErrorData.error.data; + } - const pinnedEvents = []; - for (const room of roomLists['m.server_notice']) { + if (usageLimitEventContent) { + showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error); + } else { + hideServerLimitToast(); + } + } + + _updateServerNoticeEvents = async () => { + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (!roomLists[DefaultTagID.ServerNotice]) return []; + + const events = []; + for (const room of roomLists[DefaultTagID.ServerNotice]) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; @@ -317,12 +320,18 @@ 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' + ); }); + + this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent()); }; _onPaste = (ev) => { @@ -598,12 +607,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; @@ -647,46 +650,7 @@ 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'; } @@ -701,7 +665,6 @@ class LoggedInView extends React.PureComponent { onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp} > - { topBar }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b44d66ac57..058a7ba50b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -17,12 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; -import {InvalidStoreError} from "matrix-js-sdk/src/errors"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import React, { createRef } from 'react'; +import { InvalidStoreError } from "matrix-js-sdk/src/errors"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; - // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; // what-input helps improve keyboard accessibility @@ -30,17 +29,17 @@ import 'what-input'; import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import Notifier from '../../Notifier'; import Modal from "../../Modal"; import Tinter from "../../Tinter"; import * as sdk from '../../index'; -import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite'; +import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; @@ -50,23 +49,29 @@ 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 SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; -import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; -import { ThemeWatcher } from "../../theme"; +import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; +import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; -import {defer, IDeferred} from "../../utils/promise"; +import { defer, IDeferred } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; 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 { @@ -107,7 +112,7 @@ export enum Views { // re-dispatched. NOTE: some actions are non-trivial and would require // re-factoring to be included in this list in future. const ONBOARDING_FLOW_STARTERS = [ - 'view_user_settings', + Action.ViewUserSettings, 'view_create_chat', 'view_create_room', 'view_create_group', @@ -168,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; @@ -183,7 +182,6 @@ interface IState { hideToSRUsers: boolean; syncError?: Error; resizeNotifier: ResizeNotifier; - showNotifierToolbar: boolean; serverConfig?: ValidatedServerConfig; ready: boolean; thirdPartyInvite?: object; @@ -216,6 +214,7 @@ export default class MatrixChat extends React.PureComponent { private readonly loggedInView: React.RefObject; private readonly dispatcherRef: any; private readonly themeWatcher: ThemeWatcher; + private readonly fontWatcher: FontWatcher; constructor(props, context) { super(props, context); @@ -226,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, }; @@ -283,8 +275,11 @@ export default class MatrixChat extends React.PureComponent { this.accountPasswordTimer = null; this.dispatcherRef = dis.register(this.onAction); + this.themeWatcher = new ThemeWatcher(); + this.fontWatcher = new FontWatcher(); this.themeWatcher.start(); + this.fontWatcher.start(); this.focusComposer = false; @@ -334,12 +329,6 @@ export default class MatrixChat extends React.PureComponent { }); } - if (SettingsStore.getValue("showCookieBar")) { - this.setState({ - showCookieBar: true, - }); - } - if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } @@ -367,6 +356,7 @@ export default class MatrixChat extends React.PureComponent { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); + this.fontWatcher.stop(); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); @@ -613,7 +603,7 @@ export default class MatrixChat extends React.PureComponent { case 'view_indexed_room': this.viewIndexedRoom(payload.roomIndex); break; - case 'view_user_settings': { + case Action.ViewUserSettings: { const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -680,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, @@ -730,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; @@ -755,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; } }; @@ -926,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, }); @@ -1245,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() { @@ -1372,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent { this.firstSyncComplete = true; this.firstSyncPromise.resolve(); + if (Notifier.shouldShowToolbar()) { + showNotificationsToast(); + } + dis.dispatch({action: 'focus_composer'}); this.setState({ ready: true, - showNotifierToolbar: Notifier.shouldShowToolbar(), }); }); cli.on('Call.incoming', function(call) { @@ -1454,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( @@ -1553,7 +1533,7 @@ export default class MatrixChat extends React.PureComponent { icon: "verification", props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), - priority: ToastStore.PRIORITY_REALTIME, + priority: 90, }); } }); @@ -1621,9 +1601,7 @@ export default class MatrixChat extends React.PureComponent { action: 'view_create_room', }); } else if (screen === 'settings') { - dis.dispatch({ - action: 'view_user_settings', - }); + dis.fire(Action.ViewUserSettings); } else if (screen === 'welcome') { dis.dispatch({ action: 'view_welcome_page', @@ -1755,8 +1733,8 @@ export default class MatrixChat extends React.PureComponent { const member = new RoomMember(null, userId); if (!member) { return; } - dis.dispatch({ - action: 'view_user', + dis.dispatch({ + action: Action.ViewUser, member: member, }); } @@ -1829,16 +1807,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) { @@ -2033,7 +2001,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 6fbfdb504b..cc72856fc4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; +import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -107,10 +108,14 @@ 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, }; - constructor() { - super(); + // Force props to be loaded for useIRCLayout + constructor(props) { + super(props); this.state = { // previous positions the read marker has been in, so we can @@ -597,6 +602,7 @@ export default class MessagePanel extends React.Component { isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} + useIRCLayout={this.props.useIRCLayout} /> , @@ -792,6 +798,15 @@ export default class MessagePanel extends React.Component { ); } + let ircResizer = null; + if (this.props.useIRCLayout) { + ircResizer = ; + } + return ( { topSpinner } { this._getEventTiles() } diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index f179cab6ad..e2a3d6e71f 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -19,7 +19,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import * as sdk from '../../index'; import { _t } from '../../languageHandler'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 34652098b3..56cc92a8f8 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -22,7 +22,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; @@ -30,6 +30,7 @@ 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"; +import {Action} from "../../dispatcher/actions"; export default class RightPanel extends React.Component { static get propTypes() { @@ -237,7 +238,7 @@ export default class RightPanel extends React.Component { // 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: "view_user", + action: Action.ViewUser, member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, }); @@ -266,7 +267,7 @@ export default class RightPanel extends React.Component { if (SettingsStore.getValue("feature_cross_signing")) { const onClose = () => { dis.dispatch({ - action: "view_user", + action: Action.ViewUser, member: null, }); }; diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 86353cd532..acaf66b206 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -20,7 +20,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import PropTypes from 'prop-types'; @@ -199,7 +199,7 @@ export default createReactClass({ let desc; if (alias) { - desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name}); + desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name}); } else { desc = _t('Remove %(name)s from the directory?', {name: name}); } @@ -216,7 +216,7 @@ export default createReactClass({ MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => { 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 639f38a119..ae628fd06a 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -25,7 +25,7 @@ import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; import * as cryptodevices from '../../cryptodevices'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; const STATUS_BAR_HIDDEN = 0; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 4e8e51c3cc..090f3de22a 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -20,7 +20,7 @@ limitations under the License. import React, {createRef} from 'react'; import classNames from 'classnames'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import * as Unread from '../../Unread'; import * as RoomNotifs from '../../RoomNotifs'; import * as FormattingUtils from '../../utils/FormattingUtils'; @@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; -import toRem from "../../utils/rem"; +import {toPx} from "../../utils/units"; // turn this on for drop & drag console debugging galore const debug = false; @@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent { setHeight = (height) => { if (this._subList.current) { - this._subList.current.style.height = toRem(height); + this._subList.current.style.height = toPx(height); } this._updateLazyRenderHeight(height); }; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e413c33efa..c87f4cc4dd 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,7 +34,7 @@ import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import * as sdk from '../../index'; import CallHandler from '../../CallHandler'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import * as ObjectUtils from '../../ObjectUtils'; @@ -164,6 +164,10 @@ export default createReactClass({ canReact: false, canReply: false, + + useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + + matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; }, @@ -193,6 +197,8 @@ export default createReactClass({ this._roomView = createRef(); this._searchResultsPanel = createRef(); + + this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); }, _onReadReceiptsChange: function() { @@ -232,7 +238,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 +539,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 +696,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; } }, @@ -1663,14 +1688,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/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 4f44c1a169..cb0114b243 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -144,6 +144,11 @@ export default createReactClass({ /* resizeNotifier: ResizeNotifier to know when middle column has changed size */ resizeNotifier: PropTypes.object, + + /* fixedChildren: allows for children to be passed which are rendered outside + * of the wrapper + */ + fixedChildren: PropTypes.node, }, getDefaultProps: function() { @@ -881,6 +886,7 @@ export default createReactClass({ return ( + { this.props.fixedChildren }
    { this.props.children } diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 0f3f8a6be9..7e9d290bce 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -19,7 +19,7 @@ import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { Key } from '../../Keyboard'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { throttle } from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 6642cce098..713ed004b0 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -22,7 +22,7 @@ import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js index 93a596baa3..4b00da3cbf 100644 --- a/src/components/structures/TagPanelButtons.js +++ b/src/components/structures/TagPanelButtons.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6a08cd78eb..95dc42fcee 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as ObjectUtils from "../../ObjectUtils"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import * as sdk from "../../index"; import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; @@ -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/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index ebd7aaae89..234dc661f9 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -22,7 +22,7 @@ import BaseAvatar from '../views/avatars/BaseAvatar'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import * as Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; const AVATAR_SIZE = 28; diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 1aec63f04e..421d1d79a7 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -19,7 +19,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import ContentMessages from '../../ContentMessages'; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import filesize from "filesize"; import { _t } from '../../languageHandler'; diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index a25b532447..7be88f9d44 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -32,7 +32,7 @@ import * as Lifecycle from '../../../Lifecycle'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; import Login from "../../../Login"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; // Phases // Show controls to configure server details diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index ede1041f8a..5d9f868f85 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {_t} from '../../../languageHandler'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; 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/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index d281656bbe..b08cf3be60 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -24,7 +24,7 @@ import classNames from 'classnames'; import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import DMRoomMap from '../../../utils/DMRoomMap'; import * as Rooms from '../../../Rooms'; import * as RoomNotifs from '../../../RoomNotifs'; diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 7313a278cc..ff1a7f1b14 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; import * as sdk from '../../../index'; import {MenuItem} from "../../structures/ContextMenu"; diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 4448ecd041..ec99c63724 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import LogoutDialog from "../dialogs/LogoutDialog"; import Modal from "../../../Modal"; @@ -27,6 +27,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MenuItem} from "../../structures/ContextMenu"; import * as sdk from "../../../index"; import {getHomePageUrl} from "../../../utils/pages"; +import {Action} from "../../../dispatcher/actions"; export default class TopLeftMenu extends React.Component { static propTypes = { @@ -134,7 +135,7 @@ export default class TopLeftMenu extends React.Component { } openSettings() { - dis.dispatch({action: 'view_user_settings'}); + dis.fire(Action.ViewUserSettings); this.closeMenu(); } 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/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 451ec9cfde..8ddd89dc65 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -24,7 +24,7 @@ import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; @@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../ import { abbreviateUrl } from '../../../utils/UrlUtils'; import {sleep} from "../../../utils/promise"; import {Key} from "../../../Keyboard"; +import {Action} from "../../../dispatcher/actions"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -615,7 +616,7 @@ export default createReactClass({ onManageSettingsClick(e) { e.preventDefault(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); this.onCancel(); }, 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/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index d465ef26a2..10285ccee0 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index df5f559e3b..e318258586 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -182,7 +182,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 = (
diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.js index 11e202b0cc..4694619601 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; @@ -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 index a3f9430476..51f905d542 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -25,7 +25,7 @@ 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"; +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"; diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js index 1ca638f0ab..7c996fbeab 100644 --- a/src/components/views/dialogs/IntegrationsDisabledDialog.js +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js @@ -18,7 +18,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; +import {Action} from "../../../dispatcher/actions"; export default class IntegrationsDisabledDialog extends React.Component { static propTypes = { @@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component { _onOpenSettingsClick = () => { this.props.onFinished(); - dis.dispatch({action: "view_user_settings"}); + dis.fire(Action.ViewUserSettings); }; render() { diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 6b7d2ff69d..c0b7582909 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -27,14 +27,17 @@ import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import * as Email from "../../../email"; import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; import {abbreviateUrl} from "../../../utils/UrlUtils"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; -import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore"; +import SettingsStore from '../../../settings/SettingsStore'; import {Key} from "../../../Keyboard"; +import {Action} from "../../../dispatcher/actions"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -342,10 +345,10 @@ export default class InviteDialog extends React.PureComponent { _buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room - // Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the + // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const taggedRooms = RoomListStore.getRoomLists(); - const dmTaggedRooms = taggedRooms[TAG_DM]; + const taggedRooms = RoomListStoreTempProxy.getRoomLists(); + const dmTaggedRooms = taggedRooms[DefaultTagID.DM]; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); @@ -901,7 +904,7 @@ export default class InviteDialog extends React.PureComponent { _onManageSettingsClick = (e) => { e.preventDefault(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); this.props.onFinished(); }; 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/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 23a6692b39..930acaa0b8 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import Modal from '../../../Modal'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c5f807b23c..c2b98cd9f3 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -27,7 +27,7 @@ import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsT import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; export default class RoomSettingsDialog extends React.Component { 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/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 210773c524..4592d921a9 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import SettingsStore from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; +import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; @@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_settingsIcon", , )); + tabs.push(new Tab( + _td("Appearance"), + "mx_UserSettingsDialog_appearanceIcon", + , + )); tabs.push(new Tab( _td("Flair"), "mx_UserSettingsDialog_flairIcon", diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 7e51e76f6c..a16202ed93 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { // `accessSecretStorage` may prompt for storage access as needed. const recoverInfo = await accessSecretStorage(async () => { return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( - this.state.backupInfo, + this.state.backupInfo, undefined, undefined, { progressCallback: this._progressCallback }, ); }); diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index d2277bd69a..7536d66653 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import AccessibleButton from './AccessibleButton'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 80db1718f6..527436b0e4 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -31,7 +31,7 @@ import AppPermission from './AppPermission'; import AppWarning from './AppWarning'; import MessageSpinner from './MessageSpinner'; import WidgetUtils from '../../../utils/WidgetUtils'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx new file mode 100644 index 0000000000..3096ac42f7 --- /dev/null +++ b/src/components/views/elements/Draggable.tsx @@ -0,0 +1,81 @@ +/* +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'; + +interface IProps { + className: string, + dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState, + onMouseUp: (event: MouseEvent) => void, +} + +interface IState { + onMouseMove: (event: MouseEvent) => void, + onMouseUp: (event: MouseEvent) => void, + location: ILocationState, +} + +export interface ILocationState { + currentX: number, + currentY: number, +} + +export default class Draggable extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = { + onMouseMove: this.onMouseMove.bind(this), + onMouseUp: this.onMouseUp.bind(this), + location: { + currentX: 0, + currentY: 0, + }, + }; + } + + private onMouseDown = (event: MouseEvent): void => { + this.setState({ + location: { + currentX: event.clientX, + currentY: event.clientY, + }, + }); + + document.addEventListener("mousemove", this.state.onMouseMove); + document.addEventListener("mouseup", this.state.onMouseUp); + } + + private onMouseUp = (event: MouseEvent): void => { + document.removeEventListener("mousemove", this.state.onMouseMove); + document.removeEventListener("mouseup", this.state.onMouseUp); + this.props.onMouseUp(event); + } + + private onMouseMove(event: MouseEvent): void { + const newLocation = this.props.dragFunc(this.state.location, event); + + this.setState({ + location: newLocation, + }); + } + + render() { + return
+ } + +} \ No newline at end of file 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/Flair.js b/src/components/views/elements/Flair.js index bc657e9e91..0f06904b68 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import FlairStore from '../../../stores/FlairStore'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx new file mode 100644 index 0000000000..596d46bf36 --- /dev/null +++ b/src/components/views/elements/IRCTimelineProfileResizer.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 from 'react'; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import Draggable, {ILocationState} from './Draggable'; + +interface IProps { + // Current room + roomId: string, + minWidth: number, + maxWidth: number, +}; + +interface IState { + width: number, + IRCLayoutRoot: HTMLElement, +}; + +export default class IRCTimelineProfileResizer extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId), + IRCLayoutRoot: null, + } + }; + + componentDidMount() { + this.setState({ + IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement, + }, () => this.updateCSSWidth(this.state.width)) + } + + private dragFunc = (location: ILocationState, event: React.MouseEvent): ILocationState => { + const offset = event.clientX - location.currentX; + const newWidth = this.state.width + offset; + + console.log({offset}) + // If we're trying to go smaller than min width, don't. + if (newWidth < this.props.minWidth) { + return location; + } + + if (newWidth > this.props.maxWidth) { + return location; + } + + this.setState({ + width: newWidth, + }); + + this.updateCSSWidth.bind(this)(newWidth); + + return { + currentX: event.clientX, + currentY: location.currentY, + } + } + + private updateCSSWidth(newWidth: number) { + this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px"); + } + + private onMoueUp(event: MouseEvent) { + if (this.props.roomId) { + SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width); + } + } + + render() { + return + } +}; \ No newline at end of file diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 53f2501f19..06f025f236 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import ResizeObserver from 'resize-observer-polyfill'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -156,16 +156,70 @@ 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 + 'px', - left: parentRect.left + 'px', + top: (parentRect.top - clipRect.top) + 'px', + left: (parentRect.left - clipRect.left) + 'px', width: parentRect.width + 'px', height: parentRect.height + 'px', + overflow: "hidden", }); } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 1df12738c5..03a1aeed85 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; import { Room, RoomMember } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; @@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import FlairStore from "../../../stores/FlairStore"; import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {Action} from "../../../dispatcher/actions"; // For URLs of matrix.to links in the timeline which have been reformatted by // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) @@ -191,7 +192,7 @@ const Pill = createReactClass({ onUserPillClicked: function() { dis.dispatch({ - action: 'view_user', + action: Action.ViewUser, member: this.state.member, }); }, diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index eae2d13f8a..e7f7196ac6 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -19,7 +19,7 @@ import React from 'react'; import * as sdk from '../../../index'; import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import {wantsDateSeparator} from '../../../DateUtils'; import {MatrixEvent} from 'matrix-js-sdk'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; @@ -37,6 +37,8 @@ export default class ReplyThread extends React.Component { // called when the ReplyThread contents has changed, including EventTiles thereof onHeightChanged: PropTypes.func.isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, + // Specifies which layout to use. + useIRCLayout: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -176,12 +178,17 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { if (!ReplyThread.getParentEventId(parentEv)) { - return
; + return
; } - return ; + return ; } componentDidMount() { @@ -331,11 +338,13 @@ export default class ReplyThread extends React.Component { onHeightChanged={this.props.onHeightChanged} permalinkCreator={this.props.permalinkCreator} isRedacted={ev.isRedacted()} - isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} /> + isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} + useIRCLayout={this.props.useIRCLayout} + /> ; }); - return
+ return
{ header }
{ evTiles }
; 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/Slider.tsx b/src/components/views/elements/Slider.tsx new file mode 100644 index 0000000000..f76a4684d3 --- /dev/null +++ b/src/components/views/elements/Slider.tsx @@ -0,0 +1,146 @@ +/* +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'; + +interface IProps { + // A callback for the selected value + onSelectionChange: (value: number) => void; + + // The current value of the slider + value: number; + + // The range and values of the slider + // Currently only supports an ascending, constant interval range + values: number[]; + + // A function for formatting the the values + displayFunc: (value: number) => string; + + // Whether the slider is disabled + disabled: boolean; +} + +export default class Slider extends React.Component { + // offset is a terrible inverse approximation. + // if the values represents some function f(x) = y where x is the + // index of the array and y = values[x] then offset(f, y) = x + // s.t f(x) = y. + // it assumes a monotonic function and interpolates linearly between + // y values. + // Offset is used for finding the location of a value on a + // non linear slider. + private offset(values: number[], value: number): number { + // the index of the first number greater than value. + let closest = values.reduce((prev, curr) => { + return (value > curr ? prev + 1 : prev); + }, 0); + + // Off the left + if (closest === 0) { + return 0; + } + + // Off the right + if (closest === values.length) { + return 100; + } + + // Now + const closestLessValue = values[closest - 1]; + const closestGreaterValue = values[closest]; + + const intervalWidth = 1 / (values.length - 1); + + const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue) + + return 100 * (closest - 1 + linearInterpolation) * intervalWidth + + } + + render(): React.ReactNode { + const dots = this.props.values.map(v => + {} : () => this.props.onSelectionChange(v)} + key={v} + disabled={this.props.disabled} + />); + + let selection = null; + + if (!this.props.disabled) { + const offset = this.offset(this.props.values, this.props.value); + selection =
+
+
+
+ } + + return
+
+
+
{} : this.onClick.bind(this)}/> + { selection } +
+
+ {dots} +
+
+
; + } + + onClick(event: React.MouseEvent) { + const width = (event.target as HTMLElement).clientWidth; + // nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX + // is supported by all modern browsers + const relativeClick = (event.nativeEvent.offsetX / width); + const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))]; + this.props.onSelectionChange(nearestValue); + } +} + +interface IDotProps { + // Callback for behavior onclick + onClick: () => void, + + // Whether the dot should appear active + active: boolean, + + // The label on the dot + label: string, + + // Whether the slider is disabled + disabled: boolean; +} + +class Dot extends React.PureComponent { + render(): React.ReactNode { + let className = "mx_Slider_dot" + if (!this.props.disabled && this.props.active) { + className += " mx_Slider_dotActive"; + } + + return +
+
+
+ {this.props.label} +
+
+ ; + } +} 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/TagTile.js b/src/components/views/elements/TagTile.js index d8983ac2ea..1af681dadc 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard'; import * as FormattingUtils from '../../../utils/FormattingUtils'; diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.tsx similarity index 68% rename from src/components/views/elements/Tooltip.js rename to src/components/views/elements/Tooltip.tsx index fd845d9db3..133b0ecf58 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'; +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..8a20a4659b 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -72,7 +72,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/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 91c930525d..bc5334f2de 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import {_t} from '../../../languageHandler'; import classNames from 'classnames'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 9f5660386b..2582cab573 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -19,7 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -28,6 +28,7 @@ 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', @@ -103,7 +104,7 @@ export default createReactClass({ ).then(() => { // return to the user list dis.dispatch({ - action: "view_user", + action: Action.ViewUser, member: null, }); }).catch((e) => { @@ -124,7 +125,7 @@ export default createReactClass({ _onCancel: function(e) { // Go back to the user list dis.dispatch({ - action: "view_user", + action: Action.ViewUser, member: null, }); }, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index ca374d1309..b42e325a89 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -19,7 +19,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import GroupStore from '../../../stores/GroupStore'; import PropTypes from 'prop-types'; import { showGroupInviteDialog } from '../../../GroupAddressPicker'; diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index f99d5c768c..05e3f6ac2a 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { GroupMemberType } from '../../../groups'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index 3a20de7eca..8c9b39675e 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 94d143b263..fd6969a49a 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { GroupRoomType } from '../../../groups'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index b845a83c2a..44ce69ee39 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { Draggable, Droppable } from 'react-beautiful-dnd'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import FlairStore from '../../../stores/FlairStore'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index f49ae1b6b1..a5b1ae26bb 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -21,7 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; export default class MKeyVerificationRequest extends React.Component { diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 0cde90e417..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. @@ -21,12 +21,10 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; -import Modal from '../../../Modal'; +import dis from '../../../dispatcher/dispatcher'; 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/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js index b5749ced97..95bc460636 100644 --- a/src/components/views/messages/RoomCreate.js +++ b/src/components/views/messages/RoomCreate.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bed93b68c3..d512b186e9 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -131,7 +131,9 @@ export default createReactClass({ return (
    - { content } +
    + { content } +
    ); }, diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 882e331675..c762b95f83 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -25,7 +25,7 @@ import * as HtmlUtils from '../../../HtmlUtils'; import {formatDate} from '../../../DateUtils'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js index f164b6c578..33d9325433 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.js +++ b/src/components/views/right_panel/GroupHeaderButtons.js @@ -23,6 +23,8 @@ import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons'; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {Action} from "../../../dispatcher/actions"; +import {ActionPayload} from "../../../dispatcher/payloads"; const GROUP_PHASES = [ RIGHT_PANEL_PHASES.GroupMemberInfo, @@ -40,10 +42,10 @@ export default class GroupHeaderButtons extends HeaderButtons { this._onRoomsClicked = this._onRoomsClicked.bind(this); } - onAction(payload) { + onAction(payload: ActionPayload) { super.onAction(payload); - if (payload.action === "view_user") { + if (payload.action === Action.ViewUser) { if (payload.member) { this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); } else { diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 03b03218ee..1c66fe5828 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -19,7 +19,7 @@ limitations under the License. */ import React from 'react'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import RightPanelStore from "../../../stores/RightPanelStore"; export const HEADER_KIND_ROOM = "room"; diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index bad89e2dbe..838727981d 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -23,6 +23,8 @@ import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons'; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {Action} from "../../../dispatcher/actions"; +import {ActionPayload} from "../../../dispatcher/payloads"; const MEMBER_PHASES = [ RIGHT_PANEL_PHASES.RoomMemberList, @@ -39,9 +41,9 @@ export default class RoomHeaderButtons extends HeaderButtons { this._onNotificationsClicked = this._onNotificationsClicked.bind(this); } - onAction(payload) { + onAction(payload: ActionPayload) { super.onAction(payload); - if (payload.action === "view_user") { + if (payload.action === Action.ViewUser) { if (payload.member) { this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); } else { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 68139e5726..9ca890af1e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -21,7 +21,7 @@ import React, {useCallback, useMemo, useState, useEffect, useContext} from 'reac import PropTypes from 'prop-types'; import classNames from 'classnames'; import {Group, RoomMember, User} from 'matrix-js-sdk'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -44,6 +44,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification'; +import {Action} from "../../../dispatcher/actions"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -841,7 +842,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, cli.removeUserFromGroup(groupId, groupMember.userId).then(() => { // return to the user list dis.dispatch({ - action: "view_user", + action: Action.ViewUser, member: null, }); }).catch((e) => { 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/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index 1e06da0cd8..1d26e956ab 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import Tinter from '../../../Tinter'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; const ROOM_COLORS = [ diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 5de355ebd7..cd00e5048c 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -23,8 +23,9 @@ import createReactClass from 'create-react-class'; import * as sdk from "../../../index"; import { _t, _td } from '../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import {Action} from "../../../dispatcher/actions"; export default createReactClass({ @@ -37,7 +38,7 @@ export default createReactClass({ _onClickUserSettings: (e) => { e.preventDefault(); e.stopPropagation(); - dis.dispatch({action: 'view_user_settings'}); + dis.fire(Action.ViewUserSettings); }, render: function() { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index b64eb33435..06dfffad30 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -21,7 +21,7 @@ import createReactClass from 'create-react-class'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import AppTile from '../elements/AppTile'; import Modal from '../../../Modal'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import * as ScalarMessaging from '../../../ScalarMessaging'; import { _t } from '../../../languageHandler'; diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index e102b0dba4..1ac68e4b12 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import * as sdk from '../../../index'; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import * as ObjectUtils from '../../../ObjectUtils'; import AppsDrawer from './AppsDrawer'; import { _t } from '../../../languageHandler'; @@ -141,6 +141,15 @@ 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"); @@ -265,7 +274,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..ba1be6a125 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) => { @@ -503,10 +509,6 @@ export default class BasicMessageEditor extends React.Component { } } - getEditableRootNode() { - return this._editorRef; - } - isModified() { return this._modifiedFlag; } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 8353940c90..b70ef6255c 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -18,7 +18,7 @@ import React from 'react'; import * as sdk from '../../../index'; import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; @@ -190,6 +190,7 @@ 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 diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 33ee8a0f63..a822875a18 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -25,7 +25,7 @@ import classNames from "classnames"; import { _t, _td } from '../../../languageHandler'; import * as TextForEvent from "../../../TextForEvent"; import * as sdk from "../../../index"; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {EventStatus} from 'matrix-js-sdk'; import {formatTime} from "../../../DateUtils"; @@ -34,7 +34,7 @@ import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import * as ObjectUtils from "../../../ObjectUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {E2E_STATE} from "./E2EIcon"; -import toRem from "../../../utils/rem"; +import {toRem} from "../../../utils/units"; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -206,6 +206,9 @@ export default createReactClass({ // whether to show reactions for this event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }, getDefaultProps: function() { @@ -400,7 +403,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 @@ -695,6 +698,9 @@ export default createReactClass({ // joins/parts/etc avatarSize = 14; needsSenderProfile = false; + } else if (this.props.useIRCLayout) { + avatarSize = 14; + needsSenderProfile = true; } else if (this.props.continuation && this.props.tileShape !== "file_grid") { // no avatar or sender profile for continuation messages avatarSize = 0; @@ -786,6 +792,19 @@ export default createReactClass({ />; } + const linkedTimestamp = + { timestamp } + ; + + 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': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); @@ -853,13 +872,13 @@ export default createReactClass({ } return (
    + { ircTimestamp } { avatar } { sender } + { ircPadlock }
    - - { timestamp } - - { !isBubbleMessage && this._renderE2EPadlock() } + { groupTimestamp } + { groupPadlock } { thread } + { ircTimestamp }
    { readAvatars }
    { sender } + { ircPadlock }
    - - { timestamp } - - { !isBubbleMessage && this._renderE2EPadlock() } + { groupTimestamp } + { groupPadlock } { thread } +
    { controls } diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js index 28fc8fc338..924385d226 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.js @@ -18,7 +18,7 @@ import React from "react"; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; import MessageEvent from "../messages/MessageEvent"; import MemberAvatar from "../avatars/MemberAvatar"; diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 85d443d55a..2fe577377d 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; import {formatDate} from '../../../DateUtils'; import Velociraptor from "../../../Velociraptor"; import * as sdk from "../../../index"; -import toRem from "../../../utils/rem"; +import {toRem} from "../../../utils/units"; let bounce = false; try { diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index b28494c65a..e7cd2b4c0d 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import RoomViewStore from '../../../stores/RoomViewStore'; diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 86c0d7ca96..fe443d720f 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -15,7 +15,7 @@ limitations under the License. */ import React, {createRef} from "react"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index db7b86da4f..5b45cfc29a 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -15,7 +15,7 @@ limitations under the License. */ import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import React from 'react'; import { _t } from '../../../languageHandler'; import PropTypes from 'prop-types'; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 289a89a206..e4290f87d9 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -29,20 +29,22 @@ import rate_limited_func from "../../../ratelimitedfunc"; import * as Rooms from '../../../Rooms'; import DMRoomMap from '../../../utils/DMRoomMap'; import TagOrderStore from '../../../stores/TagOrderStore'; -import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore'; import CustomRoomTagStore from '../../../stores/CustomRoomTagStore'; import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; import ResizeHandle from '../elements/ResizeHandle'; import CallHandler from "../../../CallHandler"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import * as sdk from "../../../index"; import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; import * as Unread from "../../../Unread"; import RoomViewStore from "../../../stores/RoomViewStore"; +import {TAG_DM} from "../../../stores/RoomListStore"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -161,7 +163,7 @@ export default createReactClass({ this.updateVisibleRooms(); }); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._delayedRefreshRoomList(); }); @@ -521,7 +523,7 @@ export default createReactClass({ }, getTagNameForRoomId: function(roomId) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); for (const tagName of Object.keys(lists)) { for (const room of lists[tagName]) { // Should be impossible, but guard anyways. @@ -541,7 +543,7 @@ export default createReactClass({ }, getRoomLists: function() { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const filteredLists = {}; @@ -773,10 +775,10 @@ export default createReactClass({ incomingCall: incomingCallIfTaggedAs('m.favourite'), }, { - list: this.state.lists[TAG_DM], + list: this.state.lists[DefaultTagID.DM], label: _t('Direct Messages'), - tagName: TAG_DM, - incomingCall: incomingCallIfTaggedAs(TAG_DM), + tagName: DefaultTagID.DM, + incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, addRoomLabel: _t("Start chat"), }, @@ -785,6 +787,7 @@ export default createReactClass({ label: _t('Rooms'), incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, + addRoomLabel: _t("Create room"), }, ]; const tagSubLists = Object.keys(this.state.lists) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx new file mode 100644 index 0000000000..d0c147c953 --- /dev/null +++ b/src/components/views/rooms/RoomList2.tsx @@ -0,0 +1,246 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 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 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 { 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"; + +/******************************************************************* + * 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 { + onKeyDown: (ev: React.KeyboardEvent) => void; + onFocus: (ev: React.FocusEvent) => void; + onBlur: (ev: React.FocusEvent) => void; + resizeNotifier: ResizeNotifier; + collapsed: boolean; + searchFilter: string; +} + +interface IState { + sublists: ITagMap; +} + +const TAG_ORDER: TagID[] = [ + // -- Community Invites Placeholder -- + + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + + // -- Custom Tags Placeholder -- + + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; +const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite; +const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority; +const ALWAYS_VISIBLE_TAGS: TagID[] = [ + DefaultTagID.DM, + DefaultTagID.Untagged, +]; + +interface ITagAesthetics { + sectionLabel: string; + addRoomLabel?: string; + onAddRoom?: (dispatcher: Dispatcher) => void; + isInvite: boolean; + defaultHidden: boolean; +} + +const TAG_AESTHETICS: { + // @ts-ignore - TS wants this to be a string but we know better + [tagId: TagID]: ITagAesthetics; +} = { + [DefaultTagID.Invite]: { + sectionLabel: _td("Invites"), + isInvite: true, + defaultHidden: false, + }, + [DefaultTagID.Favourite]: { + sectionLabel: _td("Favourites"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.DM]: { + sectionLabel: _td("Direct Messages"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Start chat"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_chat'}), + }, + [DefaultTagID.Untagged]: { + sectionLabel: _td("Rooms"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Create room"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_room'}), + }, + [DefaultTagID.LowPriority]: { + sectionLabel: _td("Low priority"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.ServerNotice]: { + sectionLabel: _td("System Alerts"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.Archived]: { + sectionLabel: _td("Historical"), + isInvite: false, + defaultHidden: true, + }, +}; + +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; + + constructor(props: IProps) { + super(props); + + this.state = {sublists: {}}; + this.loadSublistSizes(); + this.prepareLayouts(); + } + + public componentDidMount(): void { + RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { + console.log("new lists", store.orderedLists); + this.setState({sublists: store.orderedLists}); + }); + } + + 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(); + } + }, 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, + }); + } + + private renderSublists(): React.ReactElement[] { + const components: React.ReactElement[] = []; + + for (const orderedTagId of TAG_ORDER) { + if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) { + // Populate community invites if we have the chance + // TODO + } + if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) { + // Populate custom tags if needed + // TODO + } + + const orderedRooms = this.state.sublists[orderedTagId] || []; + if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) { + continue; // skip tag - not needed + } + + const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; + if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); + + const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; + components.push(); + } + + return components; + } + + public render() { + const sublists = this.renderSublists(); + return ( + + {({onKeyDownHandler}) => ( +
    {sublists}
    + )} +
    + ); + } +} diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index fe7c57d811..30e6ae9c58 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import IdentityAuthClient from '../../../IdentityAuthClient'; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx new file mode 100644 index 0000000000..e2f489b959 --- /dev/null +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -0,0 +1,226 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 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 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"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton"; +import * as FormattingUtils from '../../../utils/FormattingUtils'; +import RoomTile2 from "./RoomTile2"; + +/******************************************************************* + * 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 { + forRooms: boolean; + rooms?: Room[]; + startAsHidden: boolean; + label: string; + onAddRoom?: () => void; + addRoomLabel: string; + isInvite: boolean; + + // TODO: Collapsed state + // TODO: Height + // TODO: Group invites + // TODO: Calls + // TODO: forceExpand? + // TODO: Header clicking + // TODO: Spinner support for historical +} + +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; + } + + private get numTiles(): number { + // TODO: Account for group invites + return (this.props.rooms || []).length; + } + + private onAddRoom = (e) => { + e.stopPropagation(); + if (this.props.onAddRoom) this.props.onAddRoom(); + }; + + private renderTiles(): React.ReactElement[] { + const tiles: React.ReactElement[] = []; + + if (this.props.rooms) { + for (const room of this.props.rooms) { + tiles.push(); + } + } + + return tiles; + } + + private renderHeader(): React.ReactElement { + const notifications = !this.props.isInvite + ? RoomNotifs.aggregateNotificationCount(this.props.rooms) + : {count: 0, highlight: true}; + const notifCount = notifications.count; + const notifHighlight = notifications.highlight; + + // TODO: Title on collapsed + // TODO: Incoming call box + + let chevron = null; + if (this.hasTiles()) { + const chevronClasses = classNames({ + 'mx_RoomSubList_chevron': true, + 'mx_RoomSubList_chevronRight': false, // isCollapsed + 'mx_RoomSubList_chevronDown': true, // !isCollapsed + }); + chevron = (
    ); + } + + return ( + + {({onFocus, isActive, ref}) => { + // TODO: Use onFocus + const tabIndex = isActive ? 0 : -1; + + // TODO: Collapsed state + let badge; + if (true) { // !isCollapsed + const badgeClasses = classNames({ + 'mx_RoomSubList_badge': true, + 'mx_RoomSubList_badgeHighlight': notifHighlight, + }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works + if (notifCount > 0) { + badge = ( + +
    + {FormattingUtils.formatCount(notifCount)} +
    +
    + ); + } else if (this.props.isInvite && this.hasTiles()) { + // Render the `!` badge for invites + badge = ( + +
    + {FormattingUtils.formatCount(this.numTiles)} +
    +
    + ); + } + } + + let addRoomButton = null; + if (!!this.props.onAddRoom) { + addRoomButton = ( + + ); + } + + // TODO: a11y (see old component) + return ( +
    + + {chevron} + {this.props.label} + + {badge} + {addRoomButton} +
    + ); + }} +
    + ); + } + + public render(): React.ReactElement { + // TODO: Proper rendering + // TODO: Error boundary + + const tiles = this.renderTiles(); + + const classes = classNames({ + // TODO: Proper collapse support + 'mx_RoomSubList': true, + 'mx_RoomSubList_hidden': false, // len && isCollapsed + 'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed + }); + + let content = null; + if (tiles.length > 0) { + // TODO: Lazy list rendering + // TODO: Whatever scrolling magic needs to happen here + content = ( + + {tiles} + + ) + } + + // TODO: onKeyDown support + return ( +
    + {this.renderHeader()} + {content} +
    + ); + } +} diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 6a23fe309b..44e5ae7643 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -21,7 +21,7 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; import * as sdk from '../../../index'; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx new file mode 100644 index 0000000000..c95cd108dc --- /dev/null +++ b/src/components/views/rooms/RoomTile2.tsx @@ -0,0 +1,277 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 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 React, { createRef } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import classNames from "classnames"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import RoomAvatar from "../../views/avatars/RoomAvatar"; +import Tooltip from "../../views/elements/Tooltip"; +import dis from '../../../dispatcher/dispatcher'; +import { Key } from "../../../Keyboard"; +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 * + ******************************************************************* + * 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. * + *******************************************************************/ + +enum NotificationColor { + // Inverted (None -> Red) because we do integer comparisons on this + None, // nothing special + Bold, // no badge, show as unread + Grey, // unread notified messages + Red, // unread pings +} + +interface IProps { + room: Room; + + // TODO: Allow falsifying counts (for invites and stuff) + // TODO: Transparency? Was this ever used? + // TODO: Incoming call boxes? +} + +interface INotificationState { + symbol: string; + color: NotificationColor; +} + +interface IState { + hover: boolean; + notificationState: INotificationState; +} + +export default class RoomTile2 extends React.Component { + private roomTile = createRef(); + + // TODO: Custom status + // TODO: Lock icon + // TODO: Presence indicator + // TODO: e2e shields + // TODO: Handle changes to room aesthetics (name, join rules, etc) + // TODO: scrollIntoView? + // TODO: hover, badge, etc + // TODO: isSelected for hover effects + // TODO: Context menu + // TODO: a11y + + constructor(props: IProps) { + super(props); + + this.state = { + 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() { + 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 + // this, but instead we're kinda forced to either duplicate the code or thread a variable + // through the code paths. This feels like the least evil option. + private get roomIsInvite(): boolean { + return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite; + } + + 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()}); + } + + private getNotificationState(): INotificationState { + const state: INotificationState = { + color: NotificationColor.None, + symbol: null, + }; + + if (this.roomIsInvite) { + state.color = NotificationColor.Red; + state.symbol = "!"; + } else { + const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight'); + const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total'); + + // For a 'true count' we pick the grey notifications first because they include the + // red notifications. If we don't have a grey count for some reason we use the red + // count. If that count is broken for some reason, assume zero. This avoids us showing + // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). + const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0); + + // Note: we only set the symbol if we have an actual count. We don't want to show + // zero on badges. + + if (redNotifs > 0) { + state.color = NotificationColor.Red; + state.symbol = FormattingUtils.formatCount(trueCount); + } else if (greyNotifs > 0) { + state.color = NotificationColor.Grey; + state.symbol = FormattingUtils.formatCount(trueCount); + } else { + // We don't have any notified messages, but we might have unread messages. Let's + // find out. + const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room); + if (hasUnread) { + state.color = NotificationColor.Bold; + // no symbol for this state + } + } + } + + return state; + } + + private onTileMouseEnter = () => { + this.setState({hover: true}); + }; + + private onTileMouseLeave = () => { + this.setState({hover: false}); + }; + + private onTileClick = (ev: React.KeyboardEvent) => { + dis.dispatch({ + action: 'view_room', + // TODO: Support show_room_tile in new room list + show_room_tile: true, // make sure the room is visible in the list + room_id: this.props.room.roomId, + clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), + }); + }; + + public render(): React.ReactElement { + // TODO: Collapsed state + // TODO: Invites + // TODO: a11y proper + // TODO: Render more than bare minimum + + const hasBadge = this.state.notificationState.color > NotificationColor.Bold; + const isUnread = this.state.notificationState.color > NotificationColor.None; + const classes = classNames({ + 'mx_RoomTile': true, + // 'mx_RoomTile_selected': this.state.selected, + 'mx_RoomTile_unread': isUnread, + 'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey, + 'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red, + 'mx_RoomTile_invited': this.roomIsInvite, + // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, + 'mx_RoomTile_noBadges': !hasBadge, + // 'mx_RoomTile_transparent': this.props.transparent, + // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, + }); + + const avatarClasses = classNames({ + 'mx_RoomTile_avatar': true, + }); + + + let badge; + if (hasBadge) { + const badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + }); + badge =
    {this.state.notificationState.symbol}
    ; + } + + // TODO: the original RoomTile uses state for the room name. Do we need to? + let name = this.props.room.name; + if (typeof name !== 'string') name = ''; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + + const nameClasses = classNames({ + 'mx_RoomTile_name': true, + 'mx_RoomTile_invite': this.roomIsInvite, + 'mx_RoomTile_badgeShown': hasBadge, + }); + + // TODO: Support collapsed state properly + let tooltip = null; + if (false) { // isCollapsed + if (this.state.hover) { + tooltip = + } + } + + return ( + + + {({onFocus, isActive, ref}) => + +
    +
    + +
    +
    +
    +
    +
    + {name} +
    +
    + {badge} +
    + {tooltip} +
    + } +
    +
    + ); + } +} diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 233bb110be..30f62c55f8 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { htmlSerializeIfNeeded, @@ -312,6 +312,7 @@ export default class SendMessageComposer extends React.Component { event: null, }); } + dis.dispatch({action: "message_sent"}); } this.sendHistoryManager.save(this.model); @@ -322,13 +323,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 @@ -424,6 +420,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 +437,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/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 9d91ab04b3..fc6e80fc61 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -18,7 +18,7 @@ import {_t, _td} from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; import WidgetUtils from '../../../utils/WidgetUtils'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js index 3e6ed16aa4..3a7042ebd2 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.js +++ b/src/components/views/rooms/ThirdPartyMemberInfo.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixEvent} from "matrix-js-sdk"; import {_t} from "../../../languageHandler"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import * as sdk from "../../../index"; import Modal from "../../../Modal"; import {isValid3pidInvite} from "../../../RoomInvite"; diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 7c88573e9c..a4ff65d8f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; import * as sdk from "../../../index"; @@ -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/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 9ca591f30e..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"; -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/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js index a5150e3777..fd6a62d73a 100644 --- a/src/components/views/settings/IntegrationManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import {Key} from "../../../Keyboard"; export default class IntegrationManager extends React.Component { diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index cb37271452..23e72e2352 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -21,7 +21,7 @@ import {_t} from "../../../languageHandler"; import * as sdk from '../../../index'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; 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/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js index 9ee9c8d130..f57d5d3798 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js @@ -21,7 +21,7 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher"; +import dis from "../../../../../dispatcher/dispatcher"; export default class AdvancedRoomSettingsTab extends React.Component { static propTypes = { diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index 99882ae400..1f12396413 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -20,7 +20,7 @@ import {_t} from "../../../../../languageHandler"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; import * as sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; -import dis from "../../../../../dispatcher"; +import dis from "../../../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; export default class GeneralRoomSettingsTab extends React.Component { 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.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx new file mode 100644 index 0000000000..bcd87b290a --- /dev/null +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -0,0 +1,311 @@ +/* +Copyright 2019 New Vector Ltd +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 React from 'react'; +import {_t} from "../../../../../languageHandler"; +import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; +import * as sdk from "../../../../../index"; +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 "../../../../../settings/watchers/FontWatcher"; +import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload'; +import { Action } from '../../../../../dispatcher/actions'; +import { IValidationResult, IFieldState } from '../../../elements/Validation'; + +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).toString(), + ...this.calculateThemeState(), + customThemeUrl: "", + customThemeMessage: {isError: false, text: ""}, + useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), + }; + } + + private calculateThemeState(): IThemeState { + // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we + // show the right values for things. + + const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); + const systemThemeExplicit: boolean = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); + const themeExplicit: string = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); + + // If the user has enabled system theme matching, use that. + if (systemThemeExplicit) { + return { + theme: themeChoice, + useSystemTheme: true, + }; + } + + // If the user has set a theme explicitly, use that (no system theme matching) + if (themeExplicit) { + return { + theme: themeChoice, + useSystemTheme: false, + }; + } + + // Otherwise assume the defaults for the settings + return { + theme: themeChoice, + useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), + }; + } + + 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: string = SettingsStore.getValue('theme'); + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { + dis.dispatch({action: Action.RecheckTheme}); + this.setState({theme: oldTheme}); + }); + this.setState({theme: newTheme}); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // 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: Action.RecheckTheme, forceTheme: newTheme}); + }; + + private onUseSystemThemeChanged = (checked: boolean): void => { + this.setState({useSystemTheme: checked}); + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({action: Action.RecheckTheme}); + }; + + private onFontSizeChanged = (size: number): void => { + this.setState({fontSize: size.toString()}); + SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); + }; + + private onValidateFontSize = async ({value}: Pick): Promise => { + const parsedSize = parseFloat(value); + const min = FontWatcher.MIN_SIZE; + const max = FontWatcher.MAX_SIZE; + + if (isNaN(parsedSize)) { + return {valid: false, feedback: _t("Size must be a number")}; + } + + if (!(min <= parsedSize && parsedSize <= max)) { + return { + valid: false, + feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max}), + }; + } + + SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value); + return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; + } + + 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); + } + + 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}}); + return; + } + currentThemes.push(themeInfo); + } catch (e) { + console.error(e); + this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}}); + return; // Don't continue on error + } + + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); + this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); + + this.themeTimer = setTimeout(() => { + this.setState({customThemeMessage: {text: "", isError: false}}); + }, 3000); + }; + + private onCustomThemeChange = (e: React.ChangeEvent): void => { + this.setState({customThemeUrl: e.target.value}); + }; + + private renderThemeSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); + + const themeWatcher = new ThemeWatcher(); + let systemThemeSection: JSX.Element; + if (themeWatcher.isSystemThemeSupported()) { + systemThemeSection =
    + +
    ; + } + + let customThemeForm: JSX.Element; + if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { + let messageElement = null; + if (this.state.customThemeMessage.text) { + if (this.state.customThemeMessage.isError) { + messageElement =
    {this.state.customThemeMessage.text}
    ; + } else { + messageElement =
    {this.state.customThemeMessage.text}
    ; + } + } + customThemeForm = ( +
    +
    + + {_t("Add theme")} + {messageElement} + +
    + ); + } + + // 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)) + .sort((a, b) => a.name.localeCompare(b.name)); + const orderedThemes = [...builtInThemes, ...customThemes]; + return ( +
    + {_t("Theme")} + {systemThemeSection} + + {orderedThemes.map(theme => { + return ; + })} + + {customThemeForm} + +
    + ); + } + + private renderFontSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + return
    + {_t("Font size")} +
    +
    Aa
    + ""} + disabled={this.state.useCustomFontSize} + /> +
    Aa
    +
    + 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/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 0f82ce3a32..c7e7ce7c2c 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; -import Field from "../../../elements/Field"; import * as languageHandler from "../../../../../languageHandler"; import {SettingLevel} from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore"; @@ -27,12 +26,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import PlatformPeg from "../../../../../PlatformPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher"; +import dis from "../../../../../dispatcher/dispatcher"; import {Service, startTermsFlow} from "../../../../../Terms"; import {SERVICE_TYPES} from "matrix-js-sdk"; import IdentityAuthClient from "../../../../../IdentityAuthClient"; @@ -62,9 +60,6 @@ export default class GeneralUserSettingsTab extends React.Component { emails: [], msisdns: [], loading3pids: true, // whether or not the emails and msisdns have been loaded - ...this._calculateThemeState(), - customThemeUrl: "", - customThemeMessage: {isError: false, text: ""}, }; this.dispatcherRef = dis.register(this._onAction); @@ -93,39 +88,6 @@ export default class GeneralUserSettingsTab extends React.Component { dis.unregister(this.dispatcherRef); } - _calculateThemeState() { - // 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( - SettingLevel.DEVICE, "use_system_theme", null, false, true); - const themeExplicit = SettingsStore.getValueAt( - SettingLevel.DEVICE, "theme", null, false, true); - - // If the user has enabled system theme matching, use that. - if (systemThemeExplicit) { - return { - theme: themeChoice, - useSystemTheme: true, - }; - } - - // If the user has set a theme explicitly, use that (no system theme matching) - if (themeExplicit) { - return { - theme: themeChoice, - useSystemTheme: false, - }; - } - - // Otherwise assume the defaults for the settings - return { - theme: themeChoice, - useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), - }; - } - _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); @@ -219,33 +181,6 @@ export default class GeneralUserSettingsTab extends React.Component { PlatformPeg.get().reload(); }; - _onThemeChange = (e) => { - 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'); - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { - dis.dispatch({action: 'recheck_theme'}); - this.setState({theme: oldTheme}); - }); - this.setState({theme: newTheme}); - // The settings watcher doesn't fire until the echo comes back from the - // server, so to make the theme change immediately we need to manually - // do the dispatch now - // 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}); - }; - - _onUseSystemThemeChanged = (checked) => { - this.setState({useSystemTheme: checked}); - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({action: 'recheck_theme'}); - }; - _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -282,41 +217,6 @@ export default class GeneralUserSettingsTab extends React.Component { }); }; - _onAddCustomTheme = async () => { - let currentThemes = SettingsStore.getValue("custom_themes"); - if (!currentThemes) currentThemes = []; - currentThemes = currentThemes.map(c => c); // cheap clone - - if (this._themeTimer) { - clearTimeout(this._themeTimer); - } - - try { - const r = await fetch(this.state.customThemeUrl); - 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}}); - return; - } - currentThemes.push(themeInfo); - } catch (e) { - console.error(e); - this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}}); - return; // Don't continue on error - } - - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); - this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); - - this._themeTimer = setTimeout(() => { - this.setState({customThemeMessage: {text: "", isError: false}}); - }, 3000); - }; - - _onCustomThemeChange = (e) => { - this.setState({customThemeUrl: e.target.value}); - }; - _renderProfileSection() { return (
    @@ -401,77 +301,6 @@ export default class GeneralUserSettingsTab extends React.Component { ); } - _renderThemeSection() { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); - const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); - - const themeWatcher = new ThemeWatcher(); - let systemThemeSection; - if (themeWatcher.isSystemThemeSupported()) { - systemThemeSection =
    - -
    ; - } - - let customThemeForm; - if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { - let messageElement = null; - if (this.state.customThemeMessage.text) { - if (this.state.customThemeMessage.isError) { - messageElement =
    {this.state.customThemeMessage.text}
    ; - } else { - messageElement =
    {this.state.customThemeMessage.text}
    ; - } - } - customThemeForm = ( -
    -
    - - {_t("Add theme")} - {messageElement} - -
    - ); - } - - 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)) - .sort((a, b) => a.name.localeCompare(b.name)); - const orderedThemes = [...builtInThemes, ...customThemes]; - return ( -
    - {_t("Theme")} - {systemThemeSection} - - {orderedThemes.map(theme => { - return ; - })} - - {customThemeForm} - -
    - ); - } - _renderDiscoverySection() { const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); @@ -560,7 +389,6 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderProfileSection()} {this._renderAccountSection()} {this._renderLanguageSection()} - {this._renderThemeSection()}
    {discoWarning} {_t("Discovery")}
    {this._renderDiscoverySection()} {this._renderIntegrationManagerSection() /* Has its own title */} 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 5dd6475e6e..bed057f03d 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,7 +25,7 @@ import Analytics from "../../../../../Analytics"; import Modal from "../../../../../Modal"; import * as sdk from "../../../../.."; import {sleep} from "../../../../../utils/promise"; -import dis from "../../../../../dispatcher"; +import dis from "../../../../../dispatcher/dispatcher"; export class IgnoredUser extends React.Component { static propTypes = { diff --git a/src/components/views/toasts/BulkUnverifiedSessionsToast.js b/src/components/views/toasts/BulkUnverifiedSessionsToast.js deleted file mode 100644 index 0c40e56858..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"; -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 85% rename from src/components/views/toasts/VerificationRequestToast.js rename to src/components/views/toasts/VerificationRequestToast.tsx index 6447e87627..38e7e31989 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -14,18 +14,33 @@ 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'; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; -import dis from "../../../dispatcher"; +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/components/views/voip/CallPreview.js b/src/components/views/voip/CallPreview.js index 049dd8a3c6..c465170950 100644 --- a/src/components/views/voip/CallPreview.js +++ b/src/components/views/voip/CallPreview.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import RoomViewStore from '../../../stores/RoomViewStore'; import CallHandler from '../../../CallHandler'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; export default createReactClass({ diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index 4a5f3923e2..a0a566dfac 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -17,7 +17,7 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import CallHandler from '../../../CallHandler'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index 53e829b784..bf28fa0157 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index 51be6db81d..a51ab70da9 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -22,7 +22,7 @@ import createReactClass from 'create-react-class'; import classNames from 'classnames'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/createRoom.js b/src/createRoom.js index 2573c438c2..79e9a517ba 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -19,7 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; diff --git a/src/cryptodevices.js b/src/cryptodevices.js index f56a80e1e4..86b97364f9 100644 --- a/src/cryptodevices.js +++ b/src/cryptodevices.js @@ -16,7 +16,7 @@ limitations under the License. import Resend from './Resend'; import * as sdk from './index'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import Modal from './Modal'; import { _t } from './languageHandler'; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts new file mode 100644 index 0000000000..7e76ea5ccb --- /dev/null +++ b/src/dispatcher/actions.ts @@ -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. +*/ + +// Dispatcher actions also extend into any arbitrary string, so support that. +export type DispatcherAction = Action | string; + +export enum Action { + // TODO: Populate with actual actions + // This is lazily generated as it also includes fixing a bunch of references. Work + // that we don't really want to take on in a giant chunk. We should always define + // new actions here, and ideally when we touch existing ones we take some time to + // define them correctly. + + // When defining a new action, please use lower_scored_case with an optional class + // name prefix. For example, `RoomListStore.view_room` or `view_user_settings`. + // New definitions should also receive an accompanying interface in the payloads + // directory. + + /** + * View a user's profile. Should be used with a ViewUserPayload. + */ + ViewUser = "view_user", + + /** + * 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", +} diff --git a/src/dispatcher.js b/src/dispatcher/dispatcher.ts similarity index 50% rename from src/dispatcher.js rename to src/dispatcher/dispatcher.ts index 5dfaa11345..8330e5cd19 100644 --- a/src/dispatcher.js +++ b/src/dispatcher/dispatcher.ts @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 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,25 +16,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import { Dispatcher } from "flux"; +import { Action } from "./actions"; +import { ActionPayload, AsyncActionPayload } from "./payloads"; -import flux from "flux"; - -class MatrixDispatcher extends flux.Dispatcher { +/** + * A dispatcher for ActionPayloads (the default within the SDK). + */ +export class MatrixDispatcher extends Dispatcher { /** - * @param {Object|function} payload Required. The payload to dispatch. - * If an Object, must contain at least an 'action' key. - * If a function, must have the signature (dispatch) => {...}. - * @param {boolean=} sync Optional. Pass true to dispatch + * Dispatches an event on the dispatcher's event bus. + * @param {ActionPayload} payload Required. The payload to dispatch. + * @param {boolean=false} sync Optional. Pass true to dispatch * synchronously. This is useful for anything triggering * an operation that the browser requires user interaction - * for. + * for. Default false (async). */ - dispatch(payload, sync) { - // Allow for asynchronous dispatching by accepting payloads that have the - // type `function (dispatch) {...}` - if (typeof payload === 'function') { - payload((action) => { + dispatch(payload: T, sync = false) { + if (payload instanceof AsyncActionPayload) { + payload.fn((action: ActionPayload) => { this.dispatch(action, sync); }); return; @@ -50,9 +51,24 @@ class MatrixDispatcher extends flux.Dispatcher { setTimeout(super.dispatch.bind(this, payload), 0); } } + + /** + * Shorthand for dispatch({action: Action.WHATEVER}, sync). No additional + * properties can be included with this version. + * @param {Action} action The action to dispatch. + * @param {boolean=false} sync Whether the dispatch should be sync or not. + * @see dispatch(action: ActionPayload, sync: boolean) + */ + fire(action: Action, sync = false) { + this.dispatch({action}, sync); + } } -if (global.mxDispatcher === undefined) { - global.mxDispatcher = new MatrixDispatcher(); +export const defaultDispatcher = new MatrixDispatcher(); + +const anyGlobal = global; +if (!anyGlobal.mxDispatcher) { + anyGlobal.mxDispatcher = defaultDispatcher; } -export default global.mxDispatcher; + +export default defaultDispatcher; diff --git a/src/dispatcher/payloads.ts b/src/dispatcher/payloads.ts new file mode 100644 index 0000000000..fa45b30623 --- /dev/null +++ b/src/dispatcher/payloads.ts @@ -0,0 +1,59 @@ +/* +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 { DispatcherAction } from "./actions"; + +/** + * The base dispatch type exposed by our dispatcher. + */ +export interface ActionPayload { + [property: string]: any; // effectively makes this 'extends Object' + action: DispatcherAction; +} + +/** + * The function the dispatcher calls when ready for an AsyncActionPayload. The + * single argument is used to start a dispatch. First the dispatcher calls the + * outer function, then when the called function is ready it calls the cb + * function to issue the dispatch. It may call the callback repeatedly if needed. + */ +export type AsyncActionFn = (cb: (action: ActionPayload) => void) => void; + +/** + * An async version of ActionPayload + */ +export class AsyncActionPayload implements ActionPayload { + /** + * The function the dispatcher should call. + */ + public readonly fn: AsyncActionFn; + + /** + * @deprecated Not used on AsyncActionPayload. + */ + public get action(): DispatcherAction { + return "NOT_USED"; + } + + /** + * Create a new AsyncActionPayload with the given ready function. + * @param {AsyncActionFn} readyFn The function to be called when the + * dispatcher is ready. + */ + public constructor(readyFn: AsyncActionFn) { + this.fn = readyFn; + } +} 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/src/dispatcher/payloads/RecheckThemePayload.ts b/src/dispatcher/payloads/RecheckThemePayload.ts new file mode 100644 index 0000000000..06f7012049 --- /dev/null +++ b/src/dispatcher/payloads/RecheckThemePayload.ts @@ -0,0 +1,27 @@ +/* +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"; + +export interface RecheckThemePayload extends ActionPayload { + action: Action.RecheckTheme, + + /** + * 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/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts new file mode 100644 index 0000000000..ed602d4e24 --- /dev/null +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -0,0 +1,29 @@ +/* +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 { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface ViewUserPayload extends ActionPayload { + action: Action.ViewUser, + + /** + * The member to view. May be null or falsy to indicate that no member + * should be shown (hide whichever relevant components). + */ + member?: RoomMember; +} 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..186fb7c6db 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2211,5 +2211,59 @@ "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.": "Въведете паролата си за възстановяване за да достъпите защитената история на съобщенията и самоличността си за кръстосано-подписване за потвърждаване на другите сесии." } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index ddd5e5521b..e5568d7bfb 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í…", 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 4cb12e7df5..8f7e8ea6b6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -102,11 +102,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 +110,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 +180,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,19 +391,60 @@ "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.", "Failed to join room": "Failed to join room", + "Font scaling": "Font scaling", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "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", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", @@ -453,6 +489,7 @@ "Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session", "How fast should messages be downloaded.": "How fast should messages be downloaded.", "Manually verify all remote sessions": "Manually verify all remote sessions", + "IRC display name width": "IRC display name width", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -564,15 +601,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:", @@ -637,8 +665,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.", @@ -746,27 +772,36 @@ "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!", + "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", "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them", - "Invalid theme schema.": "Invalid theme schema.", - "Error downloading theme information.": "Error downloading theme information.", - "Theme added!": "Theme added!", "Profile": "Profile", "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", "Set a new account password...": "Set a new account password...", "Account": "Account", "Language and region": "Language and region", - "Custom theme URL": "Custom theme URL", - "Add theme": "Add theme", - "Theme": "Theme", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "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", @@ -775,7 +810,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.", @@ -797,7 +831,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.", @@ -805,7 +839,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", @@ -824,9 +857,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", @@ -926,7 +958,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", @@ -1113,6 +1145,7 @@ "Direct Messages": "Direct Messages", "Start chat": "Start chat", "Rooms": "Rooms", + "Create room": "Create room", "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", @@ -1159,6 +1192,9 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", + "Add room": "Add room", "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.", @@ -1197,11 +1233,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", @@ -1273,7 +1309,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.", @@ -1367,24 +1402,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", @@ -1394,6 +1411,7 @@ "Objects": "Objects", "Symbols": "Symbols", "Flags": "Flags", + "Categories": "Categories", "Quick Reactions": "Quick Reactions", "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", @@ -1497,12 +1515,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...", @@ -1568,7 +1586,7 @@ "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.", @@ -1583,7 +1601,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.", @@ -1654,13 +1672,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", @@ -1824,7 +1835,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", @@ -1841,7 +1852,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", @@ -2019,11 +2029,11 @@ "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", @@ -2050,9 +2060,6 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", - "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", @@ -2072,7 +2079,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", @@ -2165,22 +2171,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", @@ -2206,6 +2196,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..983ba9e7db 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,16 @@ "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)" } 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..66c07d3722 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", @@ -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.", @@ -2189,5 +2189,129 @@ "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" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c231769f27..66bb97118f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2435,5 +2435,16 @@ "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)" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 3328292be0..99d68ca943 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -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)", @@ -545,7 +545,7 @@ "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", @@ -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", @@ -671,7 +671,7 @@ "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.", + "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,8 +701,8 @@ "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", @@ -711,7 +711,7 @@ "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", "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", @@ -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,7 +860,7 @@ "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", @@ -868,9 +868,9 @@ "Share Link to User": "Compartir a ligazón co usuario", "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,154 @@ "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" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 014d6015b3..7f6d3bf1e2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2430,5 +2430,16 @@ "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)" } diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 2652433075..d74589c553 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,16 +41,16 @@ "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:", @@ -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,23 +133,23 @@ "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", "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ę", @@ -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.", @@ -700,12 +700,12 @@ "Publish this room to the public in %(domain)s's room directory?": "Paskelbti šį kambarį į viešąjį %(domain)s kambarių katalogą?", "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", @@ -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 …", @@ -897,9 +897,9 @@ "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?", + "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)", @@ -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,13 +1013,13 @@ "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ą.", @@ -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,460 @@ "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 užšifruotas visapusiu šifravimu", + "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ų atsargines kopijas, 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įgalintas", + "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 atšaukti 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": "Sukurkite naują kambarį su tuo pačiu vardu, 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ą" } 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/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..6473dad0cf 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ť", @@ -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 objaví nasledujúci 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,138 @@ "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) ž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": "Nastavenie témy zlyhalo", + "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ý podpísaný kľúč sa zhoduje s podpísaný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 hlavnú a alternatívne 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 minutu", + "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" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 7058ad67b0..6519c54065 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2424,5 +2424,15 @@ "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ë" } 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..9495c01db7 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2434,5 +2434,16 @@ "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)": "使用改進的聊天室清單(開發中 - 重新整理以套用變更)" } diff --git a/src/indexing/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.ts similarity index 97% rename from src/indexing/BaseEventIndexManager.js rename to src/indexing/BaseEventIndexManager.ts index f780c8e9ce..c40d1300ea 100644 --- a/src/indexing/BaseEventIndexManager.js +++ b/src/indexing/BaseEventIndexManager.ts @@ -1,5 +1,5 @@ /* -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. @@ -20,7 +20,7 @@ export interface MatrixEvent { content: {}; event_id: string; origin_server_ts: number; - unsigned: ?{}; + unsigned?: {}; room_id: string; } @@ -59,7 +59,7 @@ export interface SearchArgs { before_limit: number; after_limit: number; order_by_recency: boolean; - room_id: ?string; + room_id?: string; } export interface EventAndProfile { @@ -85,7 +85,7 @@ export interface IndexStats { * * Instances of this class are provided by the application. */ -export default class BaseEventIndexManager { +export default abstract class BaseEventIndexManager { /** * Does our EventIndexManager support event indexing. * @@ -119,7 +119,7 @@ export default class BaseEventIndexManager { * @return {Promise} A promise that will resolve when the was queued up for * addition. */ - async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> { + async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise { throw new Error("Unimplemented"); } @@ -188,7 +188,7 @@ export default class BaseEventIndexManager { events: [EventAndProfile], checkpoint: CrawlerCheckpoint | null, oldCheckpoint: CrawlerCheckpoint | null, - ): Promise { + ): Promise { throw new Error("Unimplemented"); } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 02151f8474..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; } @@ -489,23 +489,44 @@ export default class EventIndex extends EventEmitter { return object; }); - // Create a new checkpoint so we can continue crawling the room for - // messages. - const newCheckpoint = { - roomId: checkpoint.roomId, - token: res.end, - fullCrawl: checkpoint.fullCrawl, - direction: checkpoint.direction, - }; + let newCheckpoint; + + // The token can be null for some reason. Don't create a checkpoint + // in that case since adding it to the db will fail. + if (res.end) { + // Create a new checkpoint so we can continue crawling the room + // for messages. + newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + } 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( events, newCheckpoint, checkpoint); + + // We didn't get a valid new checkpoint from the server, nothing + // to do here anymore. + if (!newCheckpoint) { + console.log("EventIndex: The server didn't return a valid ", + "new checkpoint, not continuing the crawl.", checkpoint); + continue; + } + // If all events were already indexed we assume that we catched // up with our index and don't need to crawl the room further. // Let us delete the checkpoint in that case, otherwise push diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 5836ffd57a..9876cb1f7f 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -18,7 +18,7 @@ import {MatrixClientPeg} from "../MatrixClientPeg"; import {ALL_RULE_TYPES, BanList} from "./BanList"; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; import {_t} from "../languageHandler"; -import dis from "../dispatcher"; +import dis from "../dispatcher/dispatcher"; // TODO: Move this and related files to the js-sdk or something once finalized. diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index e5027e0d37..9f9d7898cb 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -133,7 +133,6 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_ready", String(await client.isCrossSigningReady())); - body.append("ssss_key_needs_upgrade", String(await client.secretStorageKeyNeedsUpgrade())); } } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5c6d843349..e6aa112c5f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -29,6 +29,7 @@ import ThemeController from './controllers/ThemeController'; import PushToMatrixClientController from './controllers/PushToMatrixClientController'; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; +import FontSizeController from './controllers/FontSizeController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -94,6 +95,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "feature_font_scaling": { + isFeature: true, + displayName: _td("Font scaling"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), @@ -131,12 +138,24 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_new_room_list": { + isFeature: true, + displayName: _td("Use the improved room list (in development - refresh to apply changes)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_custom_themes": { isFeature: true, displayName: _td("Support adding custom themes"), supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_irc_ui": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Use IRC layout'), + default: false, + isFeature: true, + }, "mjolnirRooms": { supportedLevels: ['account'], default: [], @@ -158,6 +177,17 @@ export const SETTINGS = { displayName: _td("Show info about bridges in room settings"), default: false, }, + "fontSize": { + displayName: _td("Font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 15, + controller: new FontSizeController(), + }, + "useCustomFontSize": { + displayName: _td("Custom font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), @@ -519,4 +549,11 @@ export const SETTINGS = { MatrixClient.prototype.setCryptoTrustCrossSignedDevices, true, ), }, + "ircDisplayNameWidth": { + // We specifically want to have room-device > device so that users may set a device default + // with a per-room override. + supportedLevels: ['room-device', 'device'], + displayName: _td("IRC display name width"), + default: 80, + }, }; diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 0122916bc3..4b18a27c6c 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -24,7 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler"; import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler"; import {_t} from '../languageHandler'; import SdkConfig from "../SdkConfig"; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {SETTINGS} from "./Settings"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import {WatchManager} from "./WatchManager"; @@ -370,6 +370,21 @@ export default class SettingsStore { return SettingsStore._getFinalValue(setting, level, roomId, null, null); } + /** + * Gets the default value of a setting. + * @param {string} settingName The name of the setting to read the value of. + * @param {String} roomId The room ID to read the setting value in, may be null. + * @return {*} The default value + */ + static getDefaultValue(settingName) { + // Verify that the setting is actually a setting + if (!SETTINGS[settingName]) { + throw new Error("Setting '" + settingName + "' does not appear to be a setting."); + } + + return SETTINGS[settingName].default; + } + static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) { let resultingValue = calculatedValue; diff --git a/src/settings/controllers/CustomStatusController.js b/src/settings/controllers/CustomStatusController.js index 0fc6619d92..031387bb6a 100644 --- a/src/settings/controllers/CustomStatusController.js +++ b/src/settings/controllers/CustomStatusController.js @@ -15,7 +15,7 @@ limitations under the License. */ import SettingController from "./SettingController"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; export default class CustomStatusController extends SettingController { onChange(level, roomId, newValue) { diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js new file mode 100644 index 0000000000..3ef01ab99b --- /dev/null +++ b/src/settings/controllers/FontSizeController.js @@ -0,0 +1,32 @@ +/* +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 SettingController from "./SettingController"; +import dis from "../../dispatcher/dispatcher"; + +export default class FontSizeController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: "update-font-size", + size: newValue, + }); + } +} diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts new file mode 100644 index 0000000000..dce9e77e9e --- /dev/null +++ b/src/settings/watchers/FontWatcher.ts @@ -0,0 +1,55 @@ +/* +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 dis from '../../dispatcher/dispatcher'; +import SettingsStore, {SettingLevel} from '../SettingsStore'; +import IWatcher from "./Watcher"; +import { toPx } from '../../utils/units'; + +export class FontWatcher implements IWatcher { + public static readonly MIN_SIZE = 13; + public static readonly MAX_SIZE = 20; + + private dispatcherRef: string; + + constructor() { + this.dispatcherRef = null; + } + + public start() { + this.setRootFontSize(SettingsStore.getValue("fontSize")); + this.dispatcherRef = dis.register(this.onAction); + } + + public stop() { + dis.unregister(this.dispatcherRef); + } + + private onAction = (payload) => { + if (payload.action === 'update-font-size') { + this.setRootFontSize(payload.size); + } + }; + + private setRootFontSize = (size) => { + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); + + if (fontSize !== size) { + SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); + } + (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/utils/rem.js b/src/settings/watchers/Watcher.ts similarity index 84% rename from src/utils/rem.js rename to src/settings/watchers/Watcher.ts index 1f18c9de05..a9f6f3f2c8 100644 --- a/src/utils/rem.js +++ b/src/settings/watchers/Watcher.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// converts a pixel value to rem. -export default function(pixelVal) { - return pixelVal / 15 + "rem"; -} +export default interface IWatcher { + start(): void + stop(): void +} \ No newline at end of file diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts new file mode 100644 index 0000000000..3519050078 --- /dev/null +++ b/src/stores/AsyncStore.ts @@ -0,0 +1,107 @@ +/* +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 AwaitLock from 'await-lock'; +import { Dispatcher } from "flux"; +import { ActionPayload } from "../dispatcher/payloads"; + +/** + * The event/channel to listen for in an AsyncStore. + */ +export const UPDATE_EVENT = "update"; + +/** + * Represents a minimal store which works similar to Flux stores. Instead + * of everything needing to happen in a dispatch cycle, everything can + * happen async to that cycle. + * + * The store operates by using Object.assign() to mutate state - it sends the + * state objects (current and new) through the function onto a new empty + * object. Because of this, it is recommended to break out your state to be as + * safe as possible. The state mutations are also locked, preventing concurrent + * writes. + * + * All updates to the store happen on the UPDATE_EVENT event channel with the + * one argument being the instance of the store. + * + * To update the state, use updateState() and preferably await the result to + * help prevent lock conflicts. + */ +export abstract class AsyncStore extends EventEmitter { + private storeState: T = {}; + private lock = new AwaitLock(); + private readonly dispatcherRef: string; + + /** + * Creates a new AsyncStore using the given dispatcher. + * @param {Dispatcher} dispatcher The dispatcher to rely upon. + */ + protected constructor(private dispatcher: Dispatcher) { + super(); + + this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this)); + } + + /** + * The current state of the store. Cannot be mutated. + */ + protected get state(): T { + return Object.freeze(this.storeState); + } + + /** + * Stops the store's listening functions, such as the listener to the dispatcher. + */ + protected stop() { + if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); + } + + /** + * Updates the state of the store. + * @param {T|*} newState The state to update in the store using Object.assign() + */ + protected async updateState(newState: T | Object) { + await this.lock.acquireAsync(); + try { + this.storeState = Object.assign({}, this.storeState, newState); + this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Resets the store's to the provided state or an empty object. + * @param {T|*} newState The new state of the store. + * @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT. + */ + protected async reset(newState: T | Object = null, quiet = false) { + await this.lock.acquireAsync(); + try { + this.storeState = (newState || {}); + if (!quiet) this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Called when the dispatcher broadcasts a dispatch event. + * @param {ActionPayload} payload The event being dispatched. + */ + protected abstract onDispatch(payload: ActionPayload); +} diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 909282c085..48c80294b4 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -13,12 +13,12 @@ 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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import * as RoomNotifs from '../RoomNotifs'; -import RoomListStore from './RoomListStore'; import EventEmitter from 'events'; import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; +import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -60,7 +60,7 @@ class CustomRoomTagStore extends EventEmitter { trailing: true, }, ); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._setState({tags: this._getUpdatedTags()}); }); dis.register(payload => this._onDispatch(payload)); @@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter { } getSortedTags() { - const roomLists = RoomListStore.getRoomLists(); + const roomLists = RoomListStoreTempProxy.getRoomLists(); const tagNames = Object.keys(this._state.tags).sort(); const prefixes = tagNames.map((name, i) => { @@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter { return; } - const newTagNames = Object.keys(RoomListStore.getRoomLists()) + const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists()) .filter((tagName) => { return !tagName.match(STANDARD_TAGS_REGEX); }).sort(); diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 78a144f755..d4097184a1 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -18,7 +18,7 @@ import EventEmitter from 'events'; import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups'; import FlairStore from './FlairStore'; import {MatrixClientPeg} from '../MatrixClientPeg'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; function parseMembersResponse(response) { return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember)); diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 904f29f7b3..a12bac7dd6 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -15,7 +15,7 @@ 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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; const INITIAL_STATE = { diff --git a/src/stores/RightPanelStore.js b/src/stores/RightPanelStore.js index 3a5605ba3f..a73f3befbb 100644 --- a/src/stores/RightPanelStore.js +++ b/src/stores/RightPanelStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {pendingVerificationRequestForUser} from '../verification'; import {Store} from 'flux/utils'; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 89edc9a8ef..c19b2f8bc2 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Store} from 'flux/utils'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import DMRoomMap from '../utils/DMRoomMap'; import * as Unread from '../Unread'; import SettingsStore from "../settings/SettingsStore"; @@ -92,11 +92,19 @@ class RoomListStore extends Store { constructor() { super(dis); + this._checkDisabled(); this._init(); this._getManualComparator = this._getManualComparator.bind(this); this._recentsComparator = this._recentsComparator.bind(this); } + _checkDisabled() { + this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.disabled) { + console.warn("👋 legacy room list store has been disabled"); + } + } + /** * Changes the sorting algorithm used by the RoomListStore. * @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants. @@ -113,6 +121,8 @@ class RoomListStore extends Store { } _init() { + if (this.disabled) return; + // Initialise state const defaultLists = { "m.server_notice": [/* { room: js-sdk room, category: string } */], @@ -140,6 +150,8 @@ class RoomListStore extends Store { } _setState(newState) { + if (this.disabled) return; + // If we're changing the lists, transparently change the presentation lists (which // is given to requesting components). This dramatically simplifies our code elsewhere // while also ensuring we don't need to update all the calling components to support @@ -156,6 +168,8 @@ class RoomListStore extends Store { } __onDispatch(payload) { + if (this.disabled) return; + const logicallyReady = this._matrixClient && this._state.ready; switch (payload.action) { case 'setting_updated': { @@ -182,6 +196,9 @@ class RoomListStore extends Store { break; } + this._checkDisabled(); + if (this.disabled) return; + // Always ensure that we set any state needed for settings here. It is possible that // setting updates trigger on startup before we are ready to sync, so we want to make // sure that the right state is in place before we actually react to those changes. diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 841734dfb7..6e5007895c 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -15,7 +15,7 @@ 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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; import {MatrixClientPeg} from '../MatrixClientPeg'; import * as sdk from '../index'; @@ -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/SessionStore.js b/src/stores/SessionStore.js index f38bc046d0..096811940c 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -14,7 +14,7 @@ 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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; const INITIAL_STATE = { diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index c05728e497..2acf531d86 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Store} from 'flux/utils'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import GroupStore from './GroupStore'; import Analytics from '../Analytics'; import * as RoomNotifs from "../RoomNotifs"; 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/README.md b/src/stores/room-list/README.md new file mode 100644 index 0000000000..82a6e841db --- /dev/null +++ b/src/stores/room-list/README.md @@ -0,0 +1,125 @@ +# Room list sorting + +It's so complicated it needs its own README. + +## Algorithms involved + +There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. +Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting +Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting +algorithm determines how rooms get ordered within tags affected by the list algorithm. + +Behaviour of the room list takes the shape of determining what features the room list supports, as well +as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which +is described later in this doc, is an example of an algorithm which makes heavy behavioural changes +to the room list. + +Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm +the power to decide when and how to apply the tag sorting, if at all. + +### Tag sorting algorithm: Alphabetical + +When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem +for the browser. All we do is a simple string comparison and expect the browser to return something +useful. + +### Tag sorting algorithm: Manual + +Manual sorting makes use of the `order` property present on all tags for a room, per the +[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values +of `order` cause rooms to appear closer to the top of the list. + +### Tag sorting algorithm: Recent + +Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +in the room list system which determines whether an event type is capable of bubbling up in the room list. +Normally events like room messages, stickers, and room security changes will be considered useful enough +to cause a shift in time. + +Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually +consistent this means that from time to time a room might plummet or skyrocket across the tag due to the +timestamp contained within the event (generated server-side by the sender's server). + +### List ordering algorithm: Natural + +This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no +behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. +Historically, it's been the only option in Riot and extremely common in most chat applications due to +its relative deterministic behaviour. + +### List ordering algorithm: Importance + +On the other end of the spectrum, this is the most complicated algorithm which exists. There's major +behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances. + +Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags +simply get the manual sorting algorithm applied to them with no further involvement from the importance +algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off +relative (perceived) importance to the user: + +* **Red**: The room has unread mentions waiting for the user. +* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically, this is the default as rooms get + set to 'All Messages'. +* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. + +Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey +above bold, etc. + +Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm +gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but +collectively the tag will be sorted into categories with red being at the top. + + + +The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. +The sticky room will remain in position on the room list regardless of other factors going on as typically +clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms +above the selected room at all times, where N is the number of rooms above the selected rooms when it was +selected. + +For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one +room above their selection at all times. If they receive another notification, and the tag ordering is +specified as Recent, they'll see the new notification go to the top position, and the one that was previously +there fall behind the sticky room. + +The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the +tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another +room, the previous sticky room gets recalculated to determine which category it needs to be in as the user +could have been scrolled up while new messages were received. + +Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what +kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user +selecting the third room (leaving 2 above it), and then having the rooms above it read on another device. +This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain +2 rooms above the sticky room. + +An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement +exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain +the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +put the sticky room in a position where it's had to decrease N will not increase N. + +## Responsibilities of the store + +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +all kinds of filtering. + +## Class breakdowns + +The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also +responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: +tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented +to the user). Various list-specific utilities are also included, though they are expected to move +somewhere more general when needed. For example, the `membership` utilities could easily be moved +elsewhere as needed. + +The various bits throughout the room list store should also have jsdoc of some kind to help describe +what they do and how they work. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts new file mode 100644 index 0000000000..108922a598 --- /dev/null +++ b/src/stores/room-list/RoomListStore2.ts @@ -0,0 +1,249 @@ +/* +Copyright 2018, 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "matrix-js-sdk/src/client"; +import SettingsStore from "../../settings/SettingsStore"; +import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; +import { Algorithm } from "./algorithms/list-ordering/Algorithm"; +import TagOrderStore from "../TagOrderStore"; +import { AsyncStore } from "../AsyncStore"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { getListAlgorithmInstance } from "./algorithms/list-ordering"; +import { ActionPayload } from "../../dispatcher/payloads"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { readReceiptChangeIsFor } from "../../utils/read-receipts"; + +interface IState { + tagsEnabled?: boolean; + + preferredSort?: SortAlgorithm; + preferredAlgorithm?: ListAlgorithm; +} + +/** + * The event/channel which is called when the room lists have been changed. Raised + * with one argument: the instance of the store. + */ +export const LISTS_UPDATE_EVENT = "lists_update"; + +class _RoomListStore extends AsyncStore { + private matrixClient: MatrixClient; + private initialListsGenerated = false; + private enabled = false; + private algorithm: Algorithm; + + private readonly watchedSettings = [ + 'RoomList.orderAlphabetically', + 'RoomList.orderByImportance', + 'feature_custom_tags', + ]; + + constructor() { + super(defaultDispatcher); + + this.checkEnabled(); + for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); + } + + public get orderedLists(): ITagMap { + if (!this.algorithm) return {}; // No tags yet. + return this.algorithm.getOrderedRooms(); + } + + // TODO: Remove enabled flag when the old RoomListStore goes away + private checkEnabled() { + this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.enabled) { + console.log("⚡ new room list store engaged"); + } + } + + private async readAndCacheSettingsFromStore() { + const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); + const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); + const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); + await this.updateState({ + tagsEnabled, + preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, + preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, + }); + this.setAlgorithmClass(); + } + + protected async onDispatch(payload: ActionPayload) { + if (payload.action === 'MatrixActions.sync') { + // Filter out anything that isn't the first PREPARED sync. + if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { + return; + } + + // TODO: Remove this once the RoomListStore becomes default + this.checkEnabled(); + if (!this.enabled) return; + + this.matrixClient = payload.matrixClient; + + // Update any settings here, as some may have happened before we were logically ready. + console.log("Regenerating room lists: Startup"); + await this.readAndCacheSettingsFromStore(); + await this.regenerateAllLists(); + } + + // TODO: Remove this once the RoomListStore becomes default + if (!this.enabled) return; + + if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { + // 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.initialListsGenerated = false; // we'll want to regenerate them + } + + // Everything below here requires a MatrixClient or some sort of logical readiness. + const logicallyReady = this.matrixClient && this.initialListsGenerated; + if (!logicallyReady) return; + + if (payload.action === 'setting_updated') { + if (this.watchedSettings.includes(payload.settingName)) { + console.log("Regenerating room lists: Settings changed"); + await this.readAndCacheSettingsFromStore(); + + await this.regenerateAllLists(); // regenerate the lists now + } + } + + if (!this.algorithm) { + // This shouldn't happen because `initialListsGenerated` implies we have an algorithm. + throw new Error("Room list store has no algorithm to process dispatcher update with"); + } + + 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). + 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 + console.log(payload); + } else if (payload.action === 'MatrixActions.Room.timeline') { + const eventPayload = (payload); // TODO: Type out the dispatcher types + + // Ignore non-live events (backfill) + if (!eventPayload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent) return; + + 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); + } else if (payload.action === 'MatrixActions.Event.decrypted') { + const eventPayload = (payload); // TODO: Type out the dispatcher types + const roomId = eventPayload.event.getRoomId(); + const room = this.matrixClient.getRoom(roomId); + if (!room) { + console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`); + return; + } + console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`); + // TODO: Check that e2e rooms are calculated correctly on initial load. + // It seems like when viewing the room the timeline is decrypted, rather than at startup. This could + // cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :( + await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); + } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { + // TODO: Update DMs + console.log(payload); + } else if (payload.action === 'MatrixActions.Room.myMembership') { + // TODO: Update room from membership change + console.log(payload); + } else if (payload.action === 'MatrixActions.Room') { + // TODO: Update room from creation/join + console.log(payload); + } else if (payload.action === 'view_room') { + // TODO: Update sticky room + console.log(payload); + } + } + + private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); + if (shouldUpdate) { + console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`); + this.emit(LISTS_UPDATE_EVENT, this); + } + } + + private getSortAlgorithmFor(tagId: TagID): SortAlgorithm { + switch (tagId) { + case DefaultTagID.Invite: + case DefaultTagID.Untagged: + case DefaultTagID.Archived: + case DefaultTagID.LowPriority: + case DefaultTagID.DM: + return this.state.preferredSort; + case DefaultTagID.Favourite: + default: + return SortAlgorithm.Manual; + } + } + + protected async updateState(newState: IState) { + if (!this.enabled) return; + + await super.updateState(newState); + } + + private setAlgorithmClass() { + this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); + } + + private async regenerateAllLists() { + console.warn("Regenerating all room lists"); + const tags: ITagSortingMap = {}; + for (const tagId of OrderedDefaultTagIDs) { + tags[tagId] = this.getSortAlgorithmFor(tagId); + } + + if (this.state.tagsEnabled) { + // TODO: Find a more reliable way to get tags (this doesn't work) + const roomTags = TagOrderStore.getOrderedTags() || []; + console.log("rtags", roomTags); + } + + await this.algorithm.populateTags(tags); + await this.algorithm.setKnownRooms(this.matrixClient.getRooms()); + + this.initialListsGenerated = true; + + this.emit(LISTS_UPDATE_EVENT, this); + } +} + +export default class RoomListStore { + private static internalInstance: _RoomListStore; + + public static get instance(): _RoomListStore { + if (!RoomListStore.internalInstance) { + RoomListStore.internalInstance = new _RoomListStore(); + } + + return RoomListStore.internalInstance; + } +} diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts new file mode 100644 index 0000000000..0268cf0a46 --- /dev/null +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -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. +*/ + +import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore from "./RoomListStore2"; +import OldRoomListStore from "../RoomListStore"; +import { UPDATE_EVENT } from "../AsyncStore"; +import { ITagMap } from "./algorithms/models"; + +/** + * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when + * it is available to everyone. + * + * TODO: Remove this when RoomListStore gets fully replaced. + */ +export class RoomListStoreTempProxy { + public static isUsingNewStore(): boolean { + return SettingsStore.isFeatureEnabled("feature_new_room_list"); + } + + public static addListener(handler: () => void) { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.on(UPDATE_EVENT, handler); + } else { + return OldRoomListStore.addListener(handler); + } + } + + public static getRoomLists(): ITagMap { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.orderedLists; + } else { + return OldRoomListStore.getRoomLists(); + } + } +} diff --git a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts new file mode 100644 index 0000000000..e154847847 --- /dev/null +++ b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts @@ -0,0 +1,177 @@ +/* +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 { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; +import { ITagMap, ITagSortingMap } from "../models"; +import DMRoomMap from "../../../../utils/DMRoomMap"; + +// TODO: Add locking support to avoid concurrent writes? +// TODO: EventEmitter support? Might not be needed. + +/** + * 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 = {}; + protected sortAlgorithms: ITagSortingMap; + protected rooms: Room[] = []; + protected roomIdsToTags: { + [roomId: string]: TagID[]; + } = {}; + + protected constructor() { + } + + /** + * Asks the Algorithm to regenerate all lists, using the tags given + * as reference for which lists to generate and which way to generate + * them. + * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + return this.setKnownRooms(this.rooms); + } + + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + public getOrderedRooms(): ITagMap { + return this.cached; + } + + /** + * Seeds the Algorithm with a set of rooms. The algorithm will discard all + * previously known information and instead use these rooms instead. + * @param {Room[]} rooms The rooms to force the algorithm to use. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags: ITagMap = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // 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; + return; + } + + // Split out the easy rooms first (leave and invite) + const memberships = splitRoomsByMembership(rooms); + for (const room of memberships[EffectiveMembership.Invite]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`); + newTags[DefaultTagID.Invite].push(room); + } + for (const room of memberships[EffectiveMembership.Leave]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`); + newTags[DefaultTagID.Archived].push(room); + } + + // Now process all the joined rooms. This is a bit more complicated + for (const room of memberships[EffectiveMembership.Join]) { + let tags = Object.keys(room.tags || {}); + + if (tags.length === 0) { + // Check to see if it's a DM if it isn't anything else + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + tags = [DefaultTagID.DM]; + } + } + + let inTag = false; + if (tags.length > 0) { + for (const tag of tags) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); + if (!isNullOrUndefined(newTags[tag])) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`); + newTags[tag].push(room); + inTag = true; + } + } + } + + if (!inTag) { + // TODO: Determine if DM and push there instead + newTags[DefaultTagID.Untagged].push(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`); + } + } + + await this.generateFreshTags(newTags); + + this.cached = newTags; + this.updateTagsFromCache(); + } + + /** + * Updates the roomsToTags map + */ + protected updateTagsFromCache() { + const newMap = {}; + + const tags = Object.keys(this.cached); + for (const tagId of tags) { + const rooms = this.cached[tagId]; + for (const room of rooms) { + if (!newMap[room.roomId]) newMap[room.roomId] = []; + newMap[room.roomId].push(tagId); + } + } + + this.roomIdsToTags = newMap; + } + + /** + * Called when the Algorithm believes a complete regeneration of the existing + * lists is needed. + * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag + * will already have the rooms which belong to it - they just need ordering. Must + * be mutated in place. + * @returns {Promise<*>} A promise which resolves when complete. + */ + protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; + + /** + * Asks the Algorithm to update its knowledge of a room. For example, when + * a user tags a room, joins/creates a room, or leaves a room the Algorithm + * should be told that the room's info might have changed. The Algorithm + * may no-op this request if no changes are required. + * @param {Room} room The room which might have affected sorting. + * @param {RoomUpdateCause} cause The reason for the update being triggered. + * @returns {Promise} A promise which resolve to true or false + * depending on whether or not getOrderedRooms() should be called after + * processing. + */ + public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; +} diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts new file mode 100644 index 0000000000..c72cdc2e1c --- /dev/null +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -0,0 +1,298 @@ +/* +Copyright 2018, 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. +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 { Algorithm } from "./Algorithm"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomUpdateCause, TagID } from "../../models"; +import { ITagMap, SortAlgorithm } from "../models"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; +import * as Unread from '../../../../Unread'; + +/** + * The determined category of a room. + */ +export enum Category { + /** + * The room has unread mentions within. + */ + Red = "RED", + /** + * The room has unread notifications within. Note that these are not unread + * mentions - they are simply messages which the user has asked to cause a + * badge count update or push notification. + */ + Grey = "GREY", + /** + * The room has unread messages within (grey without the badge). + */ + Bold = "BOLD", + /** + * The room has no relevant unread messages within. + */ + Idle = "IDLE", +} + +interface ICategorizedRoomMap { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: Room[]; +} + +interface ICategoryIndex { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: number; // integer +} + +// Caution: changing this means you'll need to update a bunch of assumptions and +// comments! Check the usage of Category carefully to figure out what needs changing +// if you're going to change this array's order. +const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idle]; + +/** + * An implementation of the "importance" algorithm for room list sorting. Where + * the tag sorting algorithm does not interfere, rooms will be ordered into + * categories of varying importance to the user. Alphabetical sorting does not + * interfere with this algorithm, however manual ordering does. + * + * The importance of a room is defined by the kind of notifications, if any, are + * present on the room. These are classified internally as Red, Grey, Bold, and + * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy + * version of grey, and idle means all activity has been seen by the user. + * + * The algorithm works by monitoring all room changes, including new messages in + * tracked rooms, to determine if it needs a new category or different placement + * within the same category. For more information, see the comments contained + * within the class. + */ +export class ImportanceAlgorithm extends Algorithm { + + // HOW THIS WORKS + // -------------- + // + // This block of comments assumes you've read the README one level higher. + // You should do that if you haven't already. + // + // Tags are fed into the algorithmic functions from the Algorithm superclass, + // which cause subsequent updates to the room list itself. Categories within + // those tags are tracked as index numbers within the array (zero = top), with + // each sticky room being tracked separately. Internally, the category index + // 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 + // 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. + // The `indices` help track the positions of each category to make splicing easier. + + private indices: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: ICategoryIndex; + } = {}; + + // TODO: Use this (see docs above) + private stickyRoom: { + roomId: string; + tag: TagID; + fromTop: number; + } = { + roomId: null, + tag: null, + fromTop: 0, + }; + + constructor() { + super(); + console.log("Constructed an ImportanceAlgorithm"); + } + + // noinspection JSMethodCanBeStatic + private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { + const map: ICategorizedRoomMap = { + [Category.Red]: [], + [Category.Grey]: [], + [Category.Bold]: [], + [Category.Idle]: [], + }; + for (const room of rooms) { + const category = this.getRoomCategory(room); + map[category].push(room); + } + return map; + } + + // noinspection JSMethodCanBeStatic + private getRoomCategory(room: Room): Category { + // Function implementation borrowed from old RoomListStore + + const mentions = room.getUnreadNotificationCount('highlight') > 0; + if (mentions) { + return Category.Red; + } + + let unread = room.getUnreadNotificationCount() > 0; + if (unread) { + return Category.Grey; + } + + unread = Unread.doesRoomHaveUnreadMessages(room); + if (unread) { + return Category.Bold; + } + + return Category.Idle; + } + + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + if (sortBy === SortAlgorithm.Manual) { + // Manual tags essentially ignore the importance algorithm, so don't do anything + // special about them. + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } else { + // Every other sorting type affects the categories, not the whole tag. + const categorized = this.categorizeRooms(unorderedRooms); + for (const category of Object.keys(categorized)) { + const roomsToOrder = categorized[category]; + categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); + } + + const newlyOrganized: Room[] = []; + const newIndices: ICategoryIndex = {}; + + for (const category of CATEGORY_ORDER) { + newIndices[category] = newlyOrganized.length; + newlyOrganized.push(...categorized[category]); + } + + this.indices[tagId] = newIndices; + updatedTagMap[tagId] = newlyOrganized; + } + } + } + + public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + const category = this.getRoomCategory(room); + let changed = false; + for (const tag of tags) { + if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) { + continue; // Nothing to do here. + } + + const taggedRooms = this.cached[tag]; + const indices = this.indices[tag]; + let roomIdx = taggedRooms.indexOf(room); + if (roomIdx === -1) { + console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); + roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); + } + if (roomIdx === -1) { + throw new Error(`Room ${room.roomId} has no index in ${tag}`); + } + + // Try to avoid doing array operations if we don't have to: only move rooms within + // the categories if we're jumping categories + const oldCategory = this.getCategoryFromIndices(roomIdx, indices); + if (oldCategory !== category) { + // Move the room and update the indices + this.moveRoomIndexes(1, oldCategory, category, indices); + taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) + taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted) + // Note: if moveRoomIndexes() is called after the splice then the insert operation + // will happen in the wrong place. Because we would have already adjusted the index + // for the category, we don't need to determine how the room is moving in the list. + // If we instead tried to insert before updating the indices, we'd have to determine + // whether the room was moving later (towards IDLE) or earlier (towards RED) from its + // current position, as it'll affect the category's start index after we remove the + // room from the array. + } + + // The room received an update, so take out the slice and sort it. This should be relatively + // quick because the room is inserted at the top of the category, and most popular sorting + // algorithms will deal with trying to keep the active room at the top/start of the category. + // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room + // for example), the list should already be sorted well enough that it can rip through the + // array and slot the changed room in quickly. + const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] + ? Number.MAX_SAFE_INTEGER + : indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = indices[category]; + const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine + const unsortedSlice = taggedRooms.splice(startIdx, numSort); + const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); + taggedRooms.splice(startIdx, 0, ...sorted); + + // Finally, flag that we've done something + changed = true; + } + return changed; + } + + private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category { + for (let i = 0; i < CATEGORY_ORDER.length; i++) { + const category = CATEGORY_ORDER[i]; + const isLast = i === (CATEGORY_ORDER.length - 1); + const startIdx = indices[category]; + const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indices[CATEGORY_ORDER[i + 1]]; + if (index >= startIdx && index < endIdx) { + return category; + } + } + + // "Should never happen" disclaimer goes here + throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); + } + + private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) { + // We have to update the index of the category *after* the from/toCategory variables + // in order to update the indices correctly. Because the room is moving from/to those + // categories, the next category's index will change - not the category we're modifying. + // We also need to update subsequent categories as they'll all shift by nRooms, so we + // loop over the order to achieve that. + + for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indices[nextCategory] -= nRooms; + } + + for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indices[nextCategory] += nRooms; + } + + // Do a quick check to see if we've completely broken the index + for (let i = 1; i <= CATEGORY_ORDER.length; i++) { + const lastCat = CATEGORY_ORDER[i - 1]; + const thisCat = CATEGORY_ORDER[i]; + + if (indices[lastCat] > indices[thisCat]) { + // "should never happen" disclaimer goes here + console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`); + + // TODO: Regenerate index when this happens + } + } + } +} diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts new file mode 100644 index 0000000000..44a501e592 --- /dev/null +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -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 { Algorithm } from "./Algorithm"; +import { ITagMap } from "../models"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; + +/** + * Uses the natural tag sorting algorithm order to determine tag ordering. No + * additional behavioural changes are present. + */ +export class NaturalAlgorithm extends Algorithm { + + constructor() { + super(); + console.log("Constructed a NaturalAlgorithm"); + } + + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } + } + + public async handleRoomUpdate(room, cause): Promise { + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + 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]); + } + return true; // assume we changed something + } +} diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts new file mode 100644 index 0000000000..bcccd150cd --- /dev/null +++ b/src/stores/room-list/algorithms/list-ordering/index.ts @@ -0,0 +1,38 @@ +/* +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 { Algorithm } from "./Algorithm"; +import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; +import { ListAlgorithm } from "../models"; +import { NaturalAlgorithm } from "./NaturalAlgorithm"; + +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { + [ListAlgorithm.Natural]: () => new NaturalAlgorithm(), + [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), +}; + +/** + * Gets an instance of the defined algorithm + * @param {ListAlgorithm} algorithm The algorithm to get an instance of. + * @returns {Algorithm} The algorithm instance. + */ +export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { + if (!ALGORITHM_FACTORIES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_FACTORIES[algorithm](); +} diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts new file mode 100644 index 0000000000..284600a776 --- /dev/null +++ b/src/stores/room-list/algorithms/models.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 { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts new file mode 100644 index 0000000000..8d74ebd11e --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -0,0 +1,32 @@ +/* +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 { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; +import { MatrixClientPeg } from "../../../../MatrixClientPeg"; +import * as Unread from "../../../../Unread"; + +/** + * Sorts rooms according to the browser's determination of alphabetic. + */ +export class AlphabeticAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + return rooms.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts new file mode 100644 index 0000000000..6c22ee0c9c --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { TagID } from "../../models"; + +/** + * Represents a tag sorting algorithm. + */ +export interface IAlgorithm { + /** + * Sorts the given rooms according to the sorting rules of the algorithm. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag ID in which the rooms are being sorted. + * @returns {Promise} Resolves to the sorted rooms. + */ + sortRooms(rooms: Room[], tagId: TagID): Promise; +} diff --git a/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts new file mode 100644 index 0000000000..b8c0357633 --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * Sorts rooms according to the tag's `order` property on the room. + */ +export class ManualAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + const getOrderProp = (r: Room) => r.tags[tagId].order || 0; + return rooms.sort((a, b) => { + return getOrderProp(a) - getOrderProp(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts new file mode 100644 index 0000000000..df84c051f0 --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -0,0 +1,81 @@ +/* +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 { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; +import { MatrixClientPeg } from "../../../../MatrixClientPeg"; +import * as Unread from "../../../../Unread"; + +/** + * Sorts rooms according to the last event's timestamp in each room that seems + * useful to the user. + */ +export class RecentAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + // We cache the timestamp lookup to avoid iterating forever on the timeline + // of events. This cache only survives a single sort though. + // We wouldn't need this if `.sort()` didn't constantly try and compare all + // of the rooms to each other. + + // TODO: We could probably improve the sorting algorithm here by finding changes. + // For example, if we spent a little bit of time to determine which elements have + // actually changed (probably needs to be done higher up?) then we could do an + // insertion sort or similar on the limited set of changes. + + const tsCache: { [roomId: string]: number } = {}; + const getLastTs = (r: Room) => { + if (tsCache[r.roomId]) { + return tsCache[r.roomId]; + } + + const ts = (() => { + // Apparently we can have rooms without timelines, at least under testing + // environments. Just return MAX_INT when this happens. + if (!r || !r.timeline) { + return Number.MAX_SAFE_INTEGER; + } + + for (let i = r.timeline.length - 1; i >= 0; --i) { + const ev = r.timeline[i]; + if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) + + // TODO: Don't assume we're using the same client as the peg + if (ev.getSender() === MatrixClientPeg.get().getUserId() + || Unread.eventTriggersUnreadCount(ev)) { + return ev.getTs(); + } + } + + // we might only have events that don't trigger the unread indicator, + // in which case use the oldest event even if normally it wouldn't count. + // This is better than just assuming the last event was forever ago. + if (r.timeline.length && r.timeline[0].getTs()) { + return r.timeline[0].getTs(); + } else { + return Number.MAX_SAFE_INTEGER; + } + })(); + + tsCache[r.roomId] = ts; + return ts; + }; + + return rooms.sort((a, b) => { + return getLastTs(a) - getLastTs(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag-sorting/index.ts b/src/stores/room-list/algorithms/tag-sorting/index.ts new file mode 100644 index 0000000000..c22865f5ba --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/index.ts @@ -0,0 +1,53 @@ +/* +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 { SortAlgorithm } from "../models"; +import { ManualAlgorithm } from "./ManualAlgorithm"; +import { IAlgorithm } from "./IAlgorithm"; +import { TagID } from "../../models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RecentAlgorithm } from "./RecentAlgorithm"; +import { AlphabeticAlgorithm } from "./AlphabeticAlgorithm"; + +const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { + [SortAlgorithm.Recent]: new RecentAlgorithm(), + [SortAlgorithm.Alphabetic]: new AlphabeticAlgorithm(), + [SortAlgorithm.Manual]: new ManualAlgorithm(), +}; + +/** + * Gets an instance of the defined algorithm + * @param {SortAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm { + if (!ALGORITHM_INSTANCES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_INSTANCES[algorithm]; +} + +/** + * Sorts rooms in a given tag according to the algorithm given. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag in which the sorting is occurring. + * @param {SortAlgorithm} algorithm The algorithm to use for sorting. + * @returns {Promise} Resolves to the sorted rooms. + */ +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { + return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); +} diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts new file mode 100644 index 0000000000..3cb4bf146c --- /dev/null +++ b/src/stores/room-list/membership.ts @@ -0,0 +1,72 @@ +/* +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"; + +/** + * Approximation of a membership status for a given room. + */ +export enum EffectiveMembership { + /** + * The user is effectively joined to the room. For example, actually joined + * or knocking on the room (when that becomes possible). + */ + Join = "JOIN", + + /** + * The user is effectively invited to the room. Currently this is a direct map + * to the invite membership as no other membership states are effectively + * invites. + */ + Invite = "INVITE", + + /** + * The user is effectively no longer in the room. For example, kicked, + * banned, or voluntarily left. + */ + Leave = "LEAVE", +} + +export interface MembershipSplit { + // @ts-ignore - TS wants this to be a string key, but we know better. + [state: EffectiveMembership]: Room[]; +} + +export function splitRoomsByMembership(rooms: Room[]): MembershipSplit { + const split: MembershipSplit = { + [EffectiveMembership.Invite]: [], + [EffectiveMembership.Join]: [], + [EffectiveMembership.Leave]: [], + }; + + for (const room of rooms) { + split[getEffectiveMembership(room.getMyMembership())].push(room); + } + + return split; +} + +export function getEffectiveMembership(membership: string): EffectiveMembership { + if (membership === 'invite') { + return EffectiveMembership.Invite; + } else if (membership === 'join') { + // TODO: Do the same for knock? Update docs as needed in the enum. + return EffectiveMembership.Join; + } else { + // Probably a leave, kick, or ban + return EffectiveMembership.Leave; + } +} diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts new file mode 100644 index 0000000000..a0c2621077 --- /dev/null +++ b/src/stores/room-list/models.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. +*/ + +export enum DefaultTagID { + Invite = "im.vector.fake.invite", + Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms + Archived = "im.vector.fake.archived", + LowPriority = "m.lowpriority", + Favourite = "m.favourite", + DM = "im.vector.fake.direct", + ServerNotice = "m.server_notice", +} + +export const OrderedDefaultTagIDs = [ + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; + +export type TagID = string | DefaultTagID; + +export enum RoomUpdateCause { + Timeline = "TIMELINE", + RoomRead = "ROOM_READ", // TODO: Use this. +} diff --git a/src/theme.js b/src/theme.js index 2ccce81a8d..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"; -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 GeneralUserSettingsTab - - // 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..635356b9db --- /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(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/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/WidgetUtils.js b/src/utils/WidgetUtils.js index ad4c02887e..35e23f0429 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import * as url from "url"; import WidgetEchoStore from '../stores/WidgetEchoStore'; 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/res/css/views/dialogs/_EncryptedEventDialog.scss b/src/utils/units.ts similarity index 55% rename from res/css/views/dialogs/_EncryptedEventDialog.scss rename to src/utils/units.ts index ff73df509d..54dd6b0523 100644 --- a/res/css/views/dialogs/_EncryptedEventDialog.scss +++ b/src/utils/units.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; +/* Simple utils for formatting style values + */ + +// converts a pixel value to rem. +export function toRem(pixelValue: number): string { + return pixelValue / 15 + "rem"; } -.mx_EncryptedEventDialog .mx_MemberDeviceInfo_textButton { - @mixin mx_DialogButton; - background-color: $primary-bg-color; - color: $accent-color; -} - -.mx_EncryptedEventDialog button { - margin-top: 0px; +export function toPx(pixelValue: number): string { + return pixelValue + "px"; } diff --git a/src/verification.js b/src/verification.js index f488b2ebeb..289ac9544b 100644 --- a/src/verification.js +++ b/src/verification.js @@ -15,7 +15,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 59671327ce..07cd51edbd 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -206,8 +206,7 @@ describe("", () => { 'Hey ' + '' + 'Member' + + 'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member' + ''); }); }); diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 8dc4647920..d0694a8437 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -9,12 +9,12 @@ import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; import { DragDropContext } from 'react-beautiful-dnd'; -import dis from '../../../../src/dispatcher'; +import dis from '../../../../src/dispatcher/dispatcher'; import DMRoomMap from '../../../../src/utils/DMRoomMap.js'; import GroupStore from '../../../../src/stores/GroupStore.js'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; -import {TAG_DM} from "../../../../src/stores/RoomListStore"; +import {DefaultTagID} from "../../../../src/stores/room-list/models"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -153,7 +153,7 @@ describe('RoomList', () => { // Set up the room that will be moved such that it has the correct state for a room in // the section for oldTag if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}}; - if (oldTag === TAG_DM) { + if (oldTag === DefaultTagID.DM) { // Mock inverse m.direct DMRoomMap.shared().roomToUser = { [movingRoom.roomId]: '@someotheruser:domain', @@ -180,7 +180,7 @@ describe('RoomList', () => { // TODO: Re-enable dragging tests when we support dragging again. describe.skip('does correct optimistic update when dragging from', () => { it('rooms to people', () => { - expectCorrectMove(undefined, TAG_DM); + expectCorrectMove(undefined, DefaultTagID.DM); }); it('rooms to favourites', () => { @@ -195,15 +195,15 @@ describe('RoomList', () => { // Whe running the app live, it updates when some other event occurs (likely the // m.direct arriving) that these tests do not fire. xit('people to rooms', () => { - expectCorrectMove(TAG_DM, undefined); + expectCorrectMove(DefaultTagID.DM, undefined); }); it('people to favourites', () => { - expectCorrectMove(TAG_DM, 'm.favourite'); + expectCorrectMove(DefaultTagID.DM, 'm.favourite'); }); it('people to lowpriority', () => { - expectCorrectMove(TAG_DM, 'm.lowpriority'); + expectCorrectMove(DefaultTagID.DM, 'm.lowpriority'); }); it('low priority to rooms', () => { @@ -211,7 +211,7 @@ describe('RoomList', () => { }); it('low priority to people', () => { - expectCorrectMove('m.lowpriority', TAG_DM); + expectCorrectMove('m.lowpriority', DefaultTagID.DM); }); it('low priority to low priority', () => { @@ -223,7 +223,7 @@ describe('RoomList', () => { }); it('favourites to people', () => { - expectCorrectMove('m.favourite', TAG_DM); + expectCorrectMove('m.favourite', DefaultTagID.DM); }); it('favourites to low priority', () => { 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/test/test-utils.js b/test/test-utils.js index d7aa9d5de9..2d7c1bd62c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -2,7 +2,7 @@ import React from 'react'; import {MatrixClientPeg as peg} from '../src/MatrixClientPeg'; -import dis from '../src/dispatcher'; +import dis from '../src/dispatcher/dispatcher'; import {makeType} from "../src/utils/TypeUtils"; import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; import ShallowRenderer from 'react-test-renderer/shallow'; diff --git a/tsconfig.json b/tsconfig.json index b87f640734..8a01ca335e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "jsx": "react", "types": [ "node", - "react" + "react", + "flux" ] }, "include": [ diff --git a/yarn.lock b/yarn.lock index 4d96f4cb7b..d9423fc5ab 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" @@ -1218,6 +1227,19 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fbemitter@*": + version "2.0.32" + resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw= + +"@types/flux@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711" + integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA== + dependencies: + "@types/fbemitter" "*" + "@types/react" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -1228,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" @@ -1240,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" "*" @@ -1252,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" @@ -1263,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" @@ -1279,10 +1316,17 @@ dependencies: "@types/node" "*" -"@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== +"@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== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -1319,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" "*" @@ -1331,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": @@ -1536,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== @@ -1589,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" @@ -1688,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== @@ -1814,17 +1858,22 @@ 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" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.0.1.tgz#b3f65fdf66e08f7538260f79b46c15bcfc18cadd" + integrity sha512-ntLi9fzlMT/vWjC1wwVI11/cSRJ3nTS35qVekNc9WnaoMOP2eWH0RvIqwLQkDjX4a4YynsKEv+Ere2VONp9wxg== aws-sign2@~0.7.0: version "0.7.0" @@ -1832,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" @@ -1861,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" @@ -1971,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" @@ -2074,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= @@ -2083,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" @@ -2102,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" @@ -2164,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" @@ -2265,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" @@ -2352,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" @@ -2363,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" @@ -2415,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" @@ -2475,6 +2531,11 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -2587,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" @@ -2648,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== @@ -2659,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== @@ -2728,6 +2789,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -2905,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" @@ -3028,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" @@ -3061,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== @@ -3147,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" @@ -3326,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" @@ -3338,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" @@ -3351,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" @@ -3452,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" @@ -3470,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" @@ -3844,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" @@ -3914,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" @@ -4115,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" @@ -4191,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" @@ -4261,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" @@ -4362,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" @@ -4433,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== @@ -4783,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" @@ -4952,6 +5013,14 @@ istanbul-reports@^2.2.6: dependencies: html-escaper "^2.0.0" +jest-canvas-mock@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7" + integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw== + dependencies: + cssfontparser "^1.2.1" + parse-color "^1.0.0" + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -5312,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" @@ -5416,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: @@ -5618,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" @@ -5733,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.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f120533fadb309ac5dc8b2bcb6882e784ba93f18" + version "6.1.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a4a7097c103da42075f2c70e070fd01fa6fb0d48" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" @@ -5862,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" @@ -5983,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" @@ -6010,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" @@ -6098,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" @@ -6188,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" @@ -6215,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: @@ -6397,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== @@ -6409,6 +6480,13 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= + dependencies: + color-convert "~0.5.0" + parse-entities@^1.0.2, parse-entities@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" @@ -6524,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== @@ -6652,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" @@ -6686,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" @@ -6877,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" @@ -6985,12 +7063,12 @@ react-dom@^16.9.0: scheduler "^0.19.1" 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" @@ -7094,7 +7172,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== @@ -7112,12 +7190,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" @@ -7226,9 +7304,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" @@ -7410,9 +7488,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" @@ -7488,11 +7566,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" @@ -7508,10 +7584,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" @@ -7546,9 +7622,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" @@ -7588,7 +7664,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== @@ -7598,6 +7674,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" @@ -7756,9 +7837,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" @@ -7784,22 +7865,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" @@ -7983,9 +8064,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" @@ -8009,9 +8090,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" @@ -8099,15 +8180,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" @@ -8236,9 +8317,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" @@ -8388,10 +8469,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" @@ -8461,9 +8542,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" @@ -8479,9 +8560,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" @@ -8644,9 +8725,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" @@ -8778,25 +8859,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" @@ -8829,15 +8919,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" @@ -8854,13 +8944,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"