diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index a89c083518..9ecd39ffc2 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -47,7 +47,6 @@ src/components/views/rooms/UserTile.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js -src/components/views/settings/IntegrationsManager.js src/components/views/settings/Notifications.js src/GroupAddressPicker.js src/HtmlUtils.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed4db44bf..0a2ea1d48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,207 @@ +Changes in [1.2.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2) (2019-06-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.2-rc.2...v1.2.2) + +No changes since rc.2 + +Changes in [1.2.2-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2-rc.2) (2019-06-18) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.2-rc.1...v1.2.2-rc.2) + + * Defer scalar API calls until they are needed + [\#3115](https://github.com/matrix-org/matrix-react-sdk/pull/3115) + * Blend pending redactions + [\#3117](https://github.com/matrix-org/matrix-react-sdk/pull/3117) + * Keep old arrow-up behaviour when editing is not enabled + [\#3116](https://github.com/matrix-org/matrix-react-sdk/pull/3116) + * Restore Composer History under shift-up & down + [\#3098](https://github.com/matrix-org/matrix-react-sdk/pull/3098) + * Allow changing server if validation has failed + [\#3114](https://github.com/matrix-org/matrix-react-sdk/pull/3114) + * Add Upload All button to UploadConfirmDialog + [\#3109](https://github.com/matrix-org/matrix-react-sdk/pull/3109) + * Re-enable register button + [\#3112](https://github.com/matrix-org/matrix-react-sdk/pull/3112) + * keep mx_Field stretching + [\#3111](https://github.com/matrix-org/matrix-react-sdk/pull/3111) + * Fix double-spinner + [\#3107](https://github.com/matrix-org/matrix-react-sdk/pull/3107) + * Fix display of canonicalAlias in group room info + [\#3110](https://github.com/matrix-org/matrix-react-sdk/pull/3110) + * Fix welcome user + [\#3106](https://github.com/matrix-org/matrix-react-sdk/pull/3106) + * Support editing emote messages + [\#3105](https://github.com/matrix-org/matrix-react-sdk/pull/3105) + * Use flex: 1 for mx_Field to replace all the calc(100% - 20px) and more + [\#3104](https://github.com/matrix-org/matrix-react-sdk/pull/3104) + * Use overflow on MemberInfo name/mxid so that the back button stays + [\#3099](https://github.com/matrix-org/matrix-react-sdk/pull/3099) + * Allow changing servers on nonfatal errors + [\#3102](https://github.com/matrix-org/matrix-react-sdk/pull/3102) + * Simplify email registration + [\#3101](https://github.com/matrix-org/matrix-react-sdk/pull/3101) + * Allow arrow keys navigation in autocomplete list + [\#2966](https://github.com/matrix-org/matrix-react-sdk/pull/2966) + * Edit unsent messages + [\#3097](https://github.com/matrix-org/matrix-react-sdk/pull/3097) + * Fix registration with email + non-default HS + [\#3096](https://github.com/matrix-org/matrix-react-sdk/pull/3096) + * Raise action bar above read marker + [\#3095](https://github.com/matrix-org/matrix-react-sdk/pull/3095) + * Console log more helpfully + [\#3094](https://github.com/matrix-org/matrix-react-sdk/pull/3094) + +Changes in [1.2.2-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2-rc.1) (2019-06-12) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.1...v1.2.2-rc.1) + + * Align message context menu to right and vertically where space available + [\#3087](https://github.com/matrix-org/matrix-react-sdk/pull/3087) + * Allow registration to submit for non-fatal errors + [\#3093](https://github.com/matrix-org/matrix-react-sdk/pull/3093) + * Clear the login busy state after .well-known discovery + [\#3092](https://github.com/matrix-org/matrix-react-sdk/pull/3092) + * Update from Weblate + [\#3091](https://github.com/matrix-org/matrix-react-sdk/pull/3091) + * Fix registration after fail-fast + [\#3090](https://github.com/matrix-org/matrix-react-sdk/pull/3090) + * Use setBusy interface of js-sdk interactive auth + [\#3085](https://github.com/matrix-org/matrix-react-sdk/pull/3085) + * Don't handle identity server failure as fatal, and use the right message + [\#3088](https://github.com/matrix-org/matrix-react-sdk/pull/3088) + * Recheck message actions on decrypt + [\#3084](https://github.com/matrix-org/matrix-react-sdk/pull/3084) + * Fix exception on logout + [\#3086](https://github.com/matrix-org/matrix-react-sdk/pull/3086) + * Remember we were trying to accept an invite + [\#3083](https://github.com/matrix-org/matrix-react-sdk/pull/3083) + * Add funding details for GitHub sponsor button + [\#3079](https://github.com/matrix-org/matrix-react-sdk/pull/3079) + * Remove highlight from reactions + [\#3081](https://github.com/matrix-org/matrix-react-sdk/pull/3081) + * Clarify that only lowercase letters are allowed + [\#3080](https://github.com/matrix-org/matrix-react-sdk/pull/3080) + * Don't handle identity server liveliness errors as fatal + [\#3082](https://github.com/matrix-org/matrix-react-sdk/pull/3082) + * truncate long display names in timeline headings + [\#3078](https://github.com/matrix-org/matrix-react-sdk/pull/3078) + * Fail more softly on homeserver liveliness errors + [\#3067](https://github.com/matrix-org/matrix-react-sdk/pull/3067) + * Fix AddressPickerDialog adding wrong entry to selected list case + [\#3076](https://github.com/matrix-org/matrix-react-sdk/pull/3076) + * change profile keybind to backtick from i due to italics conflict + [\#3077](https://github.com/matrix-org/matrix-react-sdk/pull/3077) + * Look busy whilst requesting the email token + [\#3075](https://github.com/matrix-org/matrix-react-sdk/pull/3075) + * Fix email invites address-match checking + [\#3074](https://github.com/matrix-org/matrix-react-sdk/pull/3074) + * Add license info for Twemoji + [\#3073](https://github.com/matrix-org/matrix-react-sdk/pull/3073) + * Show read receipts on top of message + [\#3072](https://github.com/matrix-org/matrix-react-sdk/pull/3072) + * Be somewhat fuzzier when matching emojis to complete on space + [\#3070](https://github.com/matrix-org/matrix-react-sdk/pull/3070) + * Restrict reactions to a single emoji + [\#3069](https://github.com/matrix-org/matrix-react-sdk/pull/3069) + * Fix live updates to reaction row buttons + [\#3068](https://github.com/matrix-org/matrix-react-sdk/pull/3068) + * Don't refresh custom status on logout + [\#3065](https://github.com/matrix-org/matrix-react-sdk/pull/3065) + * Add a logged in class to EmbeddedPage and react to MatrixClient changes + [\#3066](https://github.com/matrix-org/matrix-react-sdk/pull/3066) + * Don't show "can't redact" dialog on network error, with redaction having + local echo & queuing now. + [\#3058](https://github.com/matrix-org/matrix-react-sdk/pull/3058) + * Fix login page breaking on wrong password + [\#3062](https://github.com/matrix-org/matrix-react-sdk/pull/3062) + * Update from Weblate + [\#3064](https://github.com/matrix-org/matrix-react-sdk/pull/3064) + * Install latest JS SDK when linting + [\#3063](https://github.com/matrix-org/matrix-react-sdk/pull/3063) + * Ensure we always show read receipts even with hidden events + [\#3056](https://github.com/matrix-org/matrix-react-sdk/pull/3056) + * Advance read receipts into trailing events without tiles + [\#3059](https://github.com/matrix-org/matrix-react-sdk/pull/3059) + * Remove unused errorText prop + [\#3061](https://github.com/matrix-org/matrix-react-sdk/pull/3061) + * Remove SettingsStore reference in RoomSettingsDialog + [\#3060](https://github.com/matrix-org/matrix-react-sdk/pull/3060) + * Custom notification sounds for rooms + [\#2928](https://github.com/matrix-org/matrix-react-sdk/pull/2928) + * Fix comments in unread room tracking + [\#3054](https://github.com/matrix-org/matrix-react-sdk/pull/3054) + * Allow source tile handler for replacements + [\#3057](https://github.com/matrix-org/matrix-react-sdk/pull/3057) + * Fix linting in MessagePanel + [\#3055](https://github.com/matrix-org/matrix-react-sdk/pull/3055) + * Convert breadcrumbs from labs to real setting + [\#3053](https://github.com/matrix-org/matrix-react-sdk/pull/3053) + * Add local echo on badges in breadcrumbs + [\#3052](https://github.com/matrix-org/matrix-react-sdk/pull/3052) + * Counteract smooth scrolling on breadcrumbs + [\#3051](https://github.com/matrix-org/matrix-react-sdk/pull/3051) + * add sbix fallback twemoji font (and bump to emoji 12) + [\#3050](https://github.com/matrix-org/matrix-react-sdk/pull/3050) + * Add option to change the default country code + [\#3049](https://github.com/matrix-org/matrix-react-sdk/pull/3049) + * Accept JSX into the GenericErrorPage and expose local session vars + [\#3043](https://github.com/matrix-org/matrix-react-sdk/pull/3043) + * Don't try and low encryption info when signing out in low bandwidth mode + [\#3048](https://github.com/matrix-org/matrix-react-sdk/pull/3048) + * only capture enter if something was selected in completions + [\#3047](https://github.com/matrix-org/matrix-react-sdk/pull/3047) + * Fix: better HTML > MD conversion for editing, including lists and quotes + [\#3040](https://github.com/matrix-org/matrix-react-sdk/pull/3040) + * Native emoji require extra line-height + [\#3044](https://github.com/matrix-org/matrix-react-sdk/pull/3044) + * port over low_bandwidth mode to develop + [\#2598](https://github.com/matrix-org/matrix-react-sdk/pull/2598) + * Fix: maintain caret at current line when position is on newline part + [\#3029](https://github.com/matrix-org/matrix-react-sdk/pull/3029) + * Remove username on HS input label + [\#3042](https://github.com/matrix-org/matrix-react-sdk/pull/3042) + * Exclude chrome in ua from safari version check for colr support + [\#3038](https://github.com/matrix-org/matrix-react-sdk/pull/3038) + * fix COLR font check being racy + [\#3034](https://github.com/matrix-org/matrix-react-sdk/pull/3034) + * Override font for usercontent download link + [\#3035](https://github.com/matrix-org/matrix-react-sdk/pull/3035) + * Revert "Make the timeline less noisy for screen readers (mk II) #3019" + [\#3033](https://github.com/matrix-org/matrix-react-sdk/pull/3033) + * Hide autocomplete on Enter key press instead of sending message + [\#2968](https://github.com/matrix-org/matrix-react-sdk/pull/2968) + * Message editing: arrow key (up/down) navigation between editable events + [\#3025](https://github.com/matrix-org/matrix-react-sdk/pull/3025) + * Message editing: fix reply text appearing in edit + [\#3032](https://github.com/matrix-org/matrix-react-sdk/pull/3032) + * Do not try to request thumbnails with non-integer widths + [\#3031](https://github.com/matrix-org/matrix-react-sdk/pull/3031) + * Message editing: preserve strikethrough as well + [\#3030](https://github.com/matrix-org/matrix-react-sdk/pull/3030) + * Add some logging for COLR checks + [\#3027](https://github.com/matrix-org/matrix-react-sdk/pull/3027) + * Fixup for tab completion: take part length into account as well + [\#3026](https://github.com/matrix-org/matrix-react-sdk/pull/3026) + * Message editing: tab completion + [\#3024](https://github.com/matrix-org/matrix-react-sdk/pull/3024) + * Message editing: dont jump to next part when inserting at *start* of + uneditable part + [\#3021](https://github.com/matrix-org/matrix-react-sdk/pull/3021) + * Message editing: preserve and re-apply formatting + [\#3013](https://github.com/matrix-org/matrix-react-sdk/pull/3013) + * Fix relationship between guests, .well-known, and auth + [\#3001](https://github.com/matrix-org/matrix-react-sdk/pull/3001) + * Restore use of full mxid login + [\#2972](https://github.com/matrix-org/matrix-react-sdk/pull/2972) + * Only expose the fallback_hs_url if the homeserver is the default homeserver + [\#2971](https://github.com/matrix-org/matrix-react-sdk/pull/2971) + * Refactor "Next" button into ServerConfig components + [\#2964](https://github.com/matrix-org/matrix-react-sdk/pull/2964) + * Render underlines and tooltips on custom server names in auth pages + [\#2965](https://github.com/matrix-org/matrix-react-sdk/pull/2965) + * Use validated server config for login, registration, and password reset + [\#2941](https://github.com/matrix-org/matrix-react-sdk/pull/2941) + Changes in [1.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.1) (2019-05-31) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0...v1.2.1) diff --git a/package.json b/package.json index a9be315b46..14d833df96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.2.1", + "version": "1.2.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -75,13 +75,13 @@ "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279", "gfm.css": "^1.1.1", "glob": "^5.0.14", - "highlight.js": "9.14.2", + "highlight.js": "^9.15.8", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "2.0.0", + "matrix-js-sdk": "2.0.1", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/res/css/_components.scss b/res/css/_components.scss index 2a91f08ee4..d30684993d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -50,7 +50,6 @@ @import "./views/context_menus/_TopLeftMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; -@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @@ -62,6 +61,7 @@ @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; +@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @@ -87,6 +87,7 @@ @import "./views/elements/_Field.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; +@import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_MemberEventListSummary.scss"; @import "./views/elements/_MessageEditor.scss"; @@ -100,8 +101,8 @@ @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; -@import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; +@import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @@ -117,7 +118,8 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; -@import "./views/messages/_ReactionDimension.scss"; +@import "./views/messages/_ReactionQuickTooltip.scss"; +@import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index b6644c1752..fa2d87029d 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -58,18 +59,6 @@ limitations under the License. border-bottom: 8px solid transparent; } -.mx_ContextualMenu_chevron_right::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-left: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - right: 1px; -} - .mx_ContextualMenu_left { left: 0; } @@ -89,18 +78,6 @@ limitations under the License. border-bottom: 8px solid transparent; } -.mx_ContextualMenu_chevron_left::after { - content: ''; - width: 0; - height: 0; - border-top: 7px solid transparent; - border-right: 7px solid $menu-bg-color; - border-bottom: 7px solid transparent; - position: absolute; - top: -7px; - left: 1px; -} - .mx_ContextualMenu_top { top: 0; } @@ -120,18 +97,6 @@ limitations under the License. border-right: 8px solid transparent; } -.mx_ContextualMenu_chevron_top::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-bottom: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - top: 1px; -} - .mx_ContextualMenu_bottom { bottom: 0; } @@ -151,24 +116,6 @@ limitations under the License. border-right: 8px solid transparent; } -.mx_ContextualMenu_chevron_bottom::after { - content: ''; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-top: 7px solid $menu-bg-color; - border-right: 7px solid transparent; - position: absolute; - left: -7px; - bottom: 1px; -} - -.mx_ContextualMenu_field { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; -} - .mx_ContextualMenu_spinner { display: block; margin: 0 auto; diff --git a/res/css/structures/_GenericErrorPage.scss b/res/css/structures/_GenericErrorPage.scss index 2b9e9f5e7d..7e9d7bbdaa 100644 --- a/res/css/structures/_GenericErrorPage.scss +++ b/res/css/structures/_GenericErrorPage.scss @@ -2,17 +2,15 @@ width: 100%; height: 100%; background-color: #fff; + display: flex; + align-items: center; + justify-content: center; } .mx_GenericErrorPage_box { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; + display: inline; width: 500px; - height: 125px; + min-height: 125px; border: 1px solid #f22; padding: 10px 10px 20px; background-color: #fcc; diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index bcfe3aefd6..1df0a61a2b 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -35,13 +35,6 @@ limitations under the License. flex: 1; } -.mx_RoomDirectory .gm-scroll-view { - // little hack because gemini doesn't seem to detect - // the scrollbar width well in this instance - // when using css scrollbars - scrollbar-width: thin; -} - .mx_RoomDirectory_createRoom { background-color: $button-bg-color; border-radius: 4px; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index a818f52125..b03d36a592 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -63,7 +63,6 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; - margin-top: 5px; height: 100%; } diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 9bcd79a357..68e5f5c19f 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -30,6 +30,7 @@ limitations under the License. .mx_Login_submit:disabled { opacity: 0.3; + cursor: default; } .mx_AuthBody a.mx_Login_sso_link:link, diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 16ac876869..cce3b5dbf5 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -72,7 +72,6 @@ limitations under the License. } .mx_Field input { - width: 100%; box-sizing: border-box; } @@ -110,7 +109,6 @@ limitations under the License. .mx_AuthBody_fieldRow > .mx_Field { margin: 0 5px; - flex: 1; } .mx_AuthBody_fieldRow > .mx_Field:first-child { diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index e2ea7d86fb..85007aeecb 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -49,10 +49,14 @@ limitations under the License. } .mx_InteractiveAuthEntryComponents_termsSubmit:disabled { - background-color: $accent-color-50pct; + background-color: $accent-color-darker; cursor: default; } .mx_InteractiveAuthEntryComponents_termsPolicy { display: block; -} \ No newline at end of file +} + +.mx_InteractiveAuthEntryComponents_passwordSection { + width: 300px; +} diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index fe96da2019..a31feb75d7 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -20,7 +20,6 @@ limitations under the License. } .mx_ServerConfig_fields .mx_Field { - flex: 1; margin: 0 5px; } diff --git a/res/css/views/dialogs/_DeactivateAccountDialog.scss b/res/css/views/dialogs/_DeactivateAccountDialog.scss index dc76da5b15..192917b2d0 100644 --- a/res/css/views/dialogs/_DeactivateAccountDialog.scss +++ b/res/css/views/dialogs/_DeactivateAccountDialog.scss @@ -21,3 +21,7 @@ limitations under the License. .mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section { margin-top: 60px; } + +.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section .mx_Field { + width: 300px; +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 1f5d36b57a..c63a1b8e7d 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -23,7 +23,11 @@ limitations under the License. cursor: default !important; } -.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button, .mx_DevTools_RoomStateExplorer_query { +.mx_DevTools_RoomStateExplorer_query { + margin-bottom: 10px; +} + +.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button { margin-bottom: 10px; width: 100%; } @@ -75,11 +79,6 @@ limitations under the License. max-width: 684px; min-height: 250px; padding: 10px; - width: 100%; -} - -.mx_DevTools_content .mx_Field_input { - display: inline-block; } .mx_DevTools_eventTypeStateKeyGroup { diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss new file mode 100644 index 0000000000..b80742bd24 --- /dev/null +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -0,0 +1,41 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_MessageEditHistoryDialog .mx_Dialog_header > .mx_Dialog_title { + text-align: center; +} + +.mx_MessageEditHistoryDialog { + display: flex; + flex-direction: column; + max-height: 60vh; +} + +.mx_MessageEditHistoryDialog_scrollPanel { + flex: 1 1 auto; +} + +.mx_MessageEditHistoryDialog_edits { + list-style-type: none; + font-size: 14px; + padding: 0; + color: $primary-fg-color; + + .mx_EventTile_line, .mx_EventTile_content { + margin-right: 0px; + } +} + diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss index 28a8b7c9d7..325ff6c6ed 100644 --- a/res/css/views/dialogs/_SetPasswordDialog.scss +++ b/res/css/views/dialogs/_SetPasswordDialog.scss @@ -21,7 +21,6 @@ limitations under the License. color: $primary-fg-color; background-color: $primary-bg-color; font-size: 15px; - width: 100%; max-width: 280px; margin-bottom: 10px; } diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index be96d811d3..51fa4c4423 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -42,12 +42,6 @@ limitations under the License. margin-right: 5px; } -.mx_EditableItemList_newItem .mx_Field input { - // Use 100% of the space available for the input, but don't let the 10px - // padding on either side of the input to push it out of alignment. - width: calc(100% - 20px); -} - .mx_EditableItemList_label { margin-bottom: 5px; -} \ No newline at end of file +} diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 147bb3b471..a6ac680116 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -18,6 +18,8 @@ limitations under the License. .mx_Field { display: flex; + flex: 1; + min-width: 0; position: relative; margin: 1em 0; border-radius: 4px; @@ -42,6 +44,7 @@ limitations under the License. padding: 8px 9px; color: $primary-fg-color; background-color: $primary-bg-color; + flex: 1; } .mx_Field select { diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss new file mode 100644 index 0000000000..a3f5b6edc2 --- /dev/null +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -0,0 +1,101 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_InteractiveTooltip_wrapper { + position: fixed; + z-index: 5000; +} + +.mx_InteractiveTooltip_background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 1.0; + z-index: 5000; +} + +.mx_InteractiveTooltip { + border-radius: 3px; + background-color: $interactive-tooltip-bg-color; + color: $interactive-tooltip-fg-color; + position: absolute; + font-size: 10px; + font-weight: 600; + padding: 6px; + z-index: 5001; +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { + top: 10px; // 8px chevron + 2px spacing +} + +.mx_InteractiveTooltip_chevron_top { + position: absolute; + left: calc(50% - 8px); + top: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-bottom: 8px solid $interactive-tooltip-bg-color; + border-right: 8px solid transparent; +} + +// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path +// by Sebastiano Guerriero (@guerriero_se) +@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { + .mx_InteractiveTooltip_chevron_top { + height: 16px; + width: 16px; + background-color: inherit; + border: none; + clip-path: polygon(0% 0%, 100% 100%, 0% 100%); + transform: rotate(135deg); + border-radius: 0 0 0 3px; + top: calc(-8px / 1.414); // sqrt(2) because of rotation + } +} + +.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { + bottom: 10px; // 8px chevron + 2px spacing +} + +.mx_InteractiveTooltip_chevron_bottom { + position: absolute; + left: calc(50% - 8px); + bottom: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-top: 8px solid $interactive-tooltip-bg-color; + border-right: 8px solid transparent; +} + +// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path +// by Sebastiano Guerriero (@guerriero_se) +@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { + .mx_InteractiveTooltip_chevron_bottom { + height: 16px; + width: 16px; + background-color: inherit; + border: none; + clip-path: polygon(0% 0%, 100% 100%, 0% 100%); + transform: rotate(-45deg); + border-radius: 0 0 0 3px; + bottom: calc(-8px / 1.414); // sqrt(2) because of rotation + } +} diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index e721b267fa..7fd99bae17 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -34,6 +34,10 @@ limitations under the License. max-height: 200px; overflow-x: auto; + &:focus { + border-color: $accent-color-50pct; + } + span.mx_UserPill, span.mx_RoomPill { padding-left: 21px; position: relative; diff --git a/res/css/views/elements/_PowerSelector.scss b/res/css/views/elements/_PowerSelector.scss index 69f3a8eebb..799f6f246e 100644 --- a/res/css/views/elements/_PowerSelector.scss +++ b/res/css/views/elements/_PowerSelector.scss @@ -20,6 +20,5 @@ limitations under the License. .mx_PowerSelector .mx_Field select, .mx_PowerSelector .mx_Field input { - width: 100%; box-sizing: border-box; } diff --git a/res/css/views/elements/_ToolTipButton.scss b/res/css/views/elements/_TooltipButton.scss similarity index 87% rename from res/css/views/elements/_ToolTipButton.scss rename to res/css/views/elements/_TooltipButton.scss index c496e67515..6ea36c800e 100644 --- a/res/css/views/elements/_ToolTipButton.scss +++ b/res/css/views/elements/_TooltipButton.scss @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ToolTipButton { +.mx_TooltipButton { display: inline-block; width: 11px; height: 11px; @@ -33,17 +34,17 @@ limitations under the License. cursor: pointer; } -.mx_ToolTipButton:hover { +.mx_TooltipButton:hover { opacity: 1.0; } -.mx_ToolTipButton_container { +.mx_TooltipButton_container { position: relative; top: -18px; left: 4px; } -.mx_ToolTipButton_helpText { +.mx_TooltipButton_helpText { width: 400px; text-align: start; line-height: 17px !important; diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 685c2bb018..7ac0e95e81 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -67,6 +67,10 @@ limitations under the License. background-color: $message-action-bar-fg-color; } +.mx_MessageActionBar_reactButton::after { + mask-image: url('$(res)/img/react.svg'); +} + .mx_MessageActionBar_replyButton::after { mask-image: url('$(res)/img/reply.svg'); } diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss index e21189c59e..e5c228aa68 100644 --- a/res/css/views/messages/_MessageTimestamp.scss +++ b/res/css/views/messages/_MessageTimestamp.scss @@ -15,4 +15,6 @@ limitations under the License. */ .mx_MessageTimestamp { + color: $event-timestamp-color; + font-size: 10px; } diff --git a/res/css/views/dialogs/_BugReportDialog.scss b/res/css/views/messages/_ReactionQuickTooltip.scss similarity index 65% rename from res/css/views/dialogs/_BugReportDialog.scss rename to res/css/views/messages/_ReactionQuickTooltip.scss index 90ef55b945..7b1611483b 100644 --- a/res/css/views/dialogs/_BugReportDialog.scss +++ b/res/css/views/messages/_ReactionQuickTooltip.scss @@ -1,5 +1,5 @@ /* -Copyright 2017 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BugReportDialog .mx_Field { - flex: 1; +.mx_ReactionsQuickTooltip_buttons { + display: grid; + grid-template-columns: repeat(4, auto); } -.mx_BugReportDialog_field_input { - // TODO: We should really apply this to all .mx_Field inputs. - // See https://github.com/vector-im/riot-web/issues/9344. - flex: 1; +.mx_ReactionsQuickTooltip_label { + text-align: center; +} + +.mx_ReactionsQuickTooltip_shortcode { + padding-left: 6px; + opacity: 0.7; } diff --git a/res/css/views/messages/_ReactionDimension.scss b/res/css/views/messages/_ReactionTooltipButton.scss similarity index 66% rename from res/css/views/messages/_ReactionDimension.scss rename to res/css/views/messages/_ReactionTooltipButton.scss index 9a891d05cf..59244ab63b 100644 --- a/res/css/views/messages/_ReactionDimension.scss +++ b/res/css/views/messages/_ReactionTooltipButton.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ReactionDimension { - width: 42px; - display: flex; - justify-content: space-evenly; +.mx_ReactionTooltipButton { + font-size: 16px; + padding: 6px; + user-select: none; + cursor: pointer; + transition: transform 0.25s; + + &:hover { + transform: scale(1.2); + } } -.mx_ReactionDimension_disabled { +.mx_ReactionTooltipButton_selected { opacity: 0.4; } diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 3b764e97b4..57c02ed3e5 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -18,3 +18,17 @@ limitations under the License. margin: 6px 0; color: $primary-fg-color; } + +.mx_ReactionsRow_showAll { + text-decoration: none; + font-size: 10px; + font-weight: 600; + margin-left: 6px; + vertical-align: top; + + &:hover, + &:link, + &:visited { + color: $accent-color; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a6194832a3..1f75373be8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -93,8 +93,6 @@ limitations under the License. display: block; visibility: hidden; white-space: nowrap; - color: $event-timestamp-color; - font-size: 10px; left: 0px; width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; @@ -169,6 +167,9 @@ limitations under the License. .mx_EventTile_sending .mx_RoomPill { opacity: 0.5; } +.mx_EventTile_sending.mx_EventTile_redacted .mx_UnknownBody { + opacity: 0.4; +} .mx_EventTile_notSent { color: $event-notsent-color; @@ -400,6 +401,7 @@ limitations under the License. color: $roomtopic-color; display: inline-block; margin-left: 9px; + cursor: pointer; } /* Various markdown overrides */ diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index c3b3ca2f7d..bb38c41581 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -43,6 +43,8 @@ limitations under the License. .mx_MemberInfo_name h2 { flex: 1; + overflow-x: auto; + max-height: 50px; } .mx_MemberInfo h2 { diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index ea3b787971..6ac5546f78 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -39,6 +39,16 @@ limitations under the License. margin: 10px 10px 10px 0; flex: 0 0 auto; } + + .mx_RoomPreviewBar_footer { + font-size: 12px; + line-height: 20px; + + .mx_Spinner { + vertical-align: middle; + display: inline-block; + } + } } .mx_RoomPreviewBar_dark { diff --git a/res/css/views/rooms/_RoomUpgradeWarningBar.scss b/res/css/views/rooms/_RoomUpgradeWarningBar.scss index fe81d3801a..1c477cedfe 100644 --- a/res/css/views/rooms/_RoomUpgradeWarningBar.scss +++ b/res/css/views/rooms/_RoomUpgradeWarningBar.scss @@ -15,17 +15,22 @@ limitations under the License. */ .mx_RoomUpgradeWarningBar { + max-height: 235px; + background-color: $preview-bar-bg-color; + padding-left: 20px; + padding-right: 20px; + overflow: scroll; +} + +.mx_RoomUpgradeWarningBar_wrapped { + width: 100%; + height: 100%; + display: flex; text-align: center; - height: 235px; - background-color: $event-selected-color; align-items: center; flex-direction: column; justify-content: center; - display: flex; - background-color: $preview-bar-bg-color; -webkit-align-items: center; - padding-left: 20px; - padding-right: 20px; } .mx_RoomUpgradeWarningBar_header { diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss index eef804a33b..4f9541af2c 100644 --- a/res/css/views/settings/_EmailAddresses.scss +++ b/res/css/views/settings/_EmailAddresses.scss @@ -35,9 +35,3 @@ limitations under the License. .mx_ExistingEmailAddress_confirmBtn { margin-right: 5px; } - -.mx_EmailAddresses_new .mx_Field input { - // Use 100% of the space available for the input, but don't let the 10px - // padding on either side of the input to push it out of alignment. - width: calc(100% - 20px); -} diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationsManager.scss index 93ee0e20fe..c5769d3645 100644 --- a/res/css/views/settings/_IntegrationsManager.scss +++ b/res/css/views/settings/_IntegrationsManager.scss @@ -29,3 +29,16 @@ limitations under the License. width: 100%; height: 100%; } + +.mx_IntegrationsManager_loading h3 { + text-align: center; +} + +.mx_IntegrationsManager_error { + text-align: center; + padding-top: 20px; +} + +.mx_IntegrationsManager_error h3 { + color: $warning-color; +} \ No newline at end of file diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss index 2f54babd6f..a3891882c2 100644 --- a/res/css/views/settings/_PhoneNumbers.scss +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -36,12 +36,6 @@ limitations under the License. margin-right: 5px; } -.mx_PhoneNumbers_new .mx_Field input { - // Use 100% of the space available for the input, but don't let the 10px - // padding on either side of the input to push it out of alignment. - width: calc(100% - 20px); -} - .mx_PhoneNumbers_input { display: flex; align-items: center; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index b2e449ac34..a972162618 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -22,11 +22,6 @@ limitations under the License. flex-grow: 1; } -.mx_ProfileSettings_controls .mx_Field #profileDisplayName, -.mx_ProfileSettings_controls .mx_Field #profileTopic { - width: calc(100% - 20px); // subtract 10px padding on left and right -} - .mx_ProfileSettings_controls .mx_Field #profileTopic { height: 4em; } diff --git a/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss index 91d7ed2c7d..af55820d66 100644 --- a/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss @@ -17,7 +17,3 @@ limitations under the License. .mx_GeneralRoomSettingsTab_profileSection { margin-top: 10px; } - -.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select { - width: 100%; -} diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index bec013674a..091c98ffb8 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,33 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword, -.mx_GeneralUserSettingsTab_themeSection { - display: block; -} - .mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_themeSection .mx_Field { - display: block; margin-right: 100px; // Align with the other fields on the page } -.mx_GeneralUserSettingsTab_changePassword .mx_Field input { - display: block; - width: calc(100% - 20px); // subtract 10px padding on left and right -} - .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child { margin-top: 0; } -.mx_GeneralUserSettingsTab_themeSection .mx_Field select { - display: block; - width: 100%; -} - .mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { margin-right: 100px; // Align with the other fields on the page -} \ No newline at end of file +} diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index f447221b7a..b3430f47af 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -17,11 +17,3 @@ limitations under the License. .mx_PreferencesUserSettingsTab .mx_Field { margin-right: 100px; // Align with the rest of the controls } - -.mx_PreferencesUserSettingsTab .mx_Field input { - display: block; - - // Subtract 10px padding on left and right - // This is to keep the input aligned with the rest of the tab's controls. - width: calc(100% - 20px); -} diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index f5dba9831e..36c8cfd896 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -14,11 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VoiceUserSettingsTab .mx_Field select { - width: 100%; - max-width: 100%; -} - .mx_VoiceUserSettingsTab .mx_Field { margin-right: 100px; // align with the rest of the fields } diff --git a/res/img/react.svg b/res/img/react.svg new file mode 100644 index 0000000000..dd23c41c2c --- /dev/null +++ b/res/img/react.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index bdccf71540..ed1cc162a0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -160,6 +160,9 @@ $reaction-row-button-selected-border-color: $accent-color; $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; +$interactive-tooltip-bg-color: $base-color; +$interactive-tooltip-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 8244485ee3..2dd193b8c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -28,7 +28,8 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color-50pct: #92caad; +$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb +$accent-color-darker: #92caad; $accent-color-alt: #238CF5; $selection-fg-color: $primary-bg-color; @@ -272,6 +273,9 @@ $reaction-row-button-selected-border-color: $accent-color; $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; +$interactive-tooltip-bg-color: #27303a; +$interactive-tooltip-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/CallHandler.js b/src/CallHandler.js index e47209eebe..5b58400ae6 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -344,7 +344,7 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working intgrations manager. Technically we could put + // check for a working integrations manager. Technically we could put // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 9a1c9d70b8..a0364f798a 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -18,6 +18,11 @@ import * as Matrix from 'matrix-js-sdk'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export default { + hasAnyLabeledDevices: async function() { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.some(d => !!d.label); + }, + getDevices: function() { // Only needed for Electron atm, though should work in modern browsers // once permission has been granted to the webapp @@ -26,8 +31,6 @@ export default { const audioinput = []; const videoinput = []; - if (devices.some((device) => !device.label)) return false; - devices.forEach((device) => { switch (device.kind) { case 'audiooutput': audiooutput.push(device); break; diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js new file mode 100644 index 0000000000..1b3fb588eb --- /dev/null +++ b/src/ComposerHistoryManager.js @@ -0,0 +1,86 @@ +//@flow +/* +Copyright 2017 Aviral Dasgupta + +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 {Value} from 'slate'; + +import _clamp from 'lodash/clamp'; + +type MessageFormat = 'rich' | 'markdown'; + +class HistoryItem { + // We store history items in their native format to ensure history is accurate + // and then convert them if our RTE has subsequently changed format. + value: Value; + format: MessageFormat = 'rich'; + + constructor(value: ?Value, format: ?MessageFormat) { + this.value = value; + this.format = format; + } + + static fromJSON(obj: Object): HistoryItem { + return new HistoryItem( + Value.fromJSON(obj.value), + obj.format, + ); + } + + toJSON(): Object { + return { + value: this.value.toJSON(), + format: this.format, + }; + } +} + +export default class ComposerHistoryManager { + history: Array = []; + prefix: string; + lastIndex: number = 0; // used for indexing the storage + currentIndex: number = 0; // used for indexing the loaded validated history Array + + constructor(roomId: string, prefix: string = 'mx_composer_history_') { + this.prefix = prefix + roomId; + + // TODO: Performance issues? + let item; + for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + try { + this.history.push( + HistoryItem.fromJSON(JSON.parse(item)), + ); + } catch (e) { + console.warn("Throwing away unserialisable history", e); + } + } + this.lastIndex = this.currentIndex; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.history.length; + } + + save(value: Value, format: MessageFormat) { + const item = new HistoryItem(value, format); + this.history.push(item); + this.currentIndex = this.history.length; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); + } + + getItem(offset: number): ?HistoryItem { + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; + } +} diff --git a/src/ContentMessages.js b/src/ContentMessages.js index ee3e8f1390..2d58622db8 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -425,19 +425,25 @@ export default class ContentMessages { } const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); + let uploadAll = false; for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; - const shouldContinue = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - onFinished: (shouldContinue) => { - resolve(shouldContinue); - }, + 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); + }, + }); }); - }); - if (!shouldContinue) break; + if (!shouldContinue) break; + } this._sendContentToRoom(file, roomId, matrixClient); } } diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 61c51d4a20..79e5206f50 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -17,9 +17,12 @@ limitations under the License. import URL from 'url'; import dis from './dispatcher'; -import IntegrationManager from './IntegrationManager'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; +import sdk from "./index"; +import Modal from "./Modal"; +import MatrixClientPeg from "./MatrixClientPeg"; +import RoomViewStore from "./stores/RoomViewStore"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -189,7 +192,14 @@ export default class FromWidgetPostMessageApi { const data = event.data.data || event.data.widgetData; const integType = (data && data.integType) ? data.integType : null; const integId = (data && data.integId) ? data.integId : null; - IntegrationManager.open(integType, integId); + + // The dialog will take care of scalar auth for us + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + screen: 'type_' + integType, + integrationId: integId, + }, "mx_IntegrationsManager"); } else if (action === 'set_always_on_screen') { // This is a new message: there is no reason to support the deprecated widgetData here const data = event.data.data; diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js deleted file mode 100644 index 165ee6390d..0000000000 --- a/src/IntegrationManager.js +++ /dev/null @@ -1,78 +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 Modal from './Modal'; -import sdk from './index'; -import SdkConfig from './SdkConfig'; -import ScalarMessaging from './ScalarMessaging'; -import ScalarAuthClient from './ScalarAuthClient'; -import RoomViewStore from './stores/RoomViewStore'; - -if (!global.mxIntegrationManager) { - global.mxIntegrationManager = {}; -} - -export default class IntegrationManager { - static _init() { - if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) { - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - ScalarMessaging.startListening(); - global.mxIntegrationManager.client = new ScalarAuthClient(); - - return global.mxIntegrationManager.client.connect().then(() => { - global.mxIntegrationManager.connected = true; - }).catch((e) => { - console.error("Failed to connect to integrations server", e); - global.mxIntegrationManager.error = e; - }); - } else { - console.error('Invalid integration manager config', SdkConfig.get()); - } - } - } - - /** - * Launch the integrations manager on the stickers integration page - * @param {string} integName integration / widget type - * @param {string} integId integration / widget ID - * @param {function} onFinished Callback to invoke on integration manager close - */ - static async open(integName, integId, onFinished) { - await IntegrationManager._init(); - if (global.mxIntegrationManager.client) { - await global.mxIntegrationManager.client.connect(); - } else { - return; - } - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - if (global.mxIntegrationManager.error || - !(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) { - console.error("Scalar error", global.mxIntegrationManager); - return; - } - const integType = 'type_' + integName; - const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ? - global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom( - {roomId: RoomViewStore.getRoomId()}, - integType, - integId, - ) : - null; - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - onFinished: onFinished, - }, "mx_IntegrationsManager"); - } -} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 574f05bf85..07499a3a87 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -51,6 +51,7 @@ interface MatrixClientCreds { class MatrixClientPeg { constructor() { this.matrixClient = null; + this._justRegisteredUserId = null; // These are the default options used when when the // client is started in 'start'. These can be altered @@ -85,6 +86,31 @@ class MatrixClientPeg { MatrixActionCreators.stop(); } + /* + * If we've registered a user ID we set this to the ID of the + * user we've just registered. If they then go & log in, we + * can send them to the welcome user (obviously this doesn't + * guarentee they'll get a chat with the welcome user). + * + * @param {string} uid The user ID of the user we've just registered + */ + setJustRegisteredUserId(uid) { + this._justRegisteredUserId = uid; + } + + /* + * Returns true if the current user has just been registered by this + * client as determined by setJustRegisteredUserId() + * + * @returns {bool} True if user has just been registered + */ + currentUserIsJustRegistered() { + return ( + this.matrixClient && + this.matrixClient.credentials.userId === this._justRegisteredUserId + ); + } + /** * Replace this MatrixClientPeg's client with a client instance that has * homeserver / identity server URLs and active credentials diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 39384b5bea..2d5e4b3136 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,13 +27,33 @@ export const MUTE = 'mute'; export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD]; export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY]; -function _shouldShowNotifBadge(roomNotifState) { - const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD]; - return showBadgeInStates.indexOf(roomNotifState) > -1; +export function shouldShowNotifBadge(roomNotifState) { + return BADGE_STATES.includes(roomNotifState); } -function _shouldShowMentionBadge(roomNotifState) { - return roomNotifState !== MUTE; +export function shouldShowMentionBadge(roomNotifState) { + return MENTION_BADGE_STATES.includes(roomNotifState); +} + +export function countRoomsWithNotif(rooms) { + return rooms.reduce((result, room, index) => { + const roomNotifState = getRoomNotifsState(room.roomId); + const highlight = room.getUnreadNotificationCount('highlight') > 0; + const notificationCount = room.getUnreadNotificationCount(); + + const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); + const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); + const isInvite = room.hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite'); + const badges = notifBadges || mentionBadges || isInvite; + + if (badges) { + result.count++; + if (highlight) { + result.highlight = true; + } + } + return result; + }, {count: 0, highlight: false}); } export function aggregateNotificationCount(rooms) { @@ -41,8 +62,8 @@ export function aggregateNotificationCount(rooms) { const highlight = room.getUnreadNotificationCount('highlight') > 0; const notificationCount = room.getUnreadNotificationCount(); - const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState); - const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState); + const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); + const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); const badges = notifBadges || mentionBadges; if (badges) { @@ -60,8 +81,8 @@ export function getRoomHasBadge(room) { const highlight = room.getUnreadNotificationCount('highlight') > 0; const notificationCount = room.getUnreadNotificationCount(); - const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState); - const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState); + const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); + const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); return notifBadges || mentionBadges; } diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 24979aff65..27d8f0d0da 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -29,6 +29,14 @@ class ScalarAuthClient { this.scalarToken = null; } + /** + * Determines if setting up a ScalarAuthClient is even possible + * @returns {boolean} true if possible, false otherwise. + */ + static isPossible() { + return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; + } + connect() { return this.getScalarToken().then((tok) => { this.scalarToken = tok; @@ -41,7 +49,8 @@ class ScalarAuthClient { // Returns a scalar_token string getScalarToken() { - const token = window.localStorage.getItem("mx_scalar_token"); + let token = this.scalarToken; + if (!token) token = window.localStorage.getItem("mx_scalar_token"); if (!token) { return this.registerForToken(); diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index edd6f79270..3ce52247d9 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; @@ -48,7 +48,6 @@ export default class ContextualMenu extends React.Component { menuWidth: PropTypes.number, menuHeight: PropTypes.number, chevronOffset: PropTypes.number, - menuColour: PropTypes.string, chevronFace: PropTypes.string, // top, bottom, left, right or none // Function to be called on menu close onFinished: PropTypes.func, @@ -157,25 +156,6 @@ export default class ContextualMenu extends React.Component { chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); } - // To override the default chevron colour, if it's been set - let chevronCSS = ""; - if (props.menuColour) { - chevronCSS = ` - .mx_ContextualMenu_chevron_left:after { - border-right-color: ${props.menuColour}; - } - .mx_ContextualMenu_chevron_right:after { - border-left-color: ${props.menuColour}; - } - .mx_ContextualMenu_chevron_top:after { - border-left-color: ${props.menuColour}; - } - .mx_ContextualMenu_chevron_bottom:after { - border-left-color: ${props.menuColour}; - } - `; - } - const chevron = hasChevron ?
: undefined; @@ -202,10 +182,6 @@ export default class ContextualMenu extends React.Component { menuStyle.height = props.menuHeight; } - if (props.menuColour) { - menuStyle["backgroundColor"] = props.menuColour; - } - if (!isNaN(Number(props.menuPaddingTop))) { menuStyle["paddingTop"] = props.menuPaddingTop; } @@ -236,7 +212,6 @@ export default class ContextualMenu extends React.Component {
{ props.hasBackground &&
} -
; } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index cdfbe26fea..7bf5400942 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd. Copyright 2017, 2018 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -861,9 +862,9 @@ export default React.createClass({ const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const Spinner = sdk.getComponent('elements.Spinner'); - const ToolTipButton = sdk.getComponent('elements.ToolTipButton'); + const TooltipButton = sdk.getComponent('elements.TooltipButton'); - const roomsHelpNode = this.state.editing ? 0) { + this._likelyTrackpadUser = true; + this._checkAgainForTrackpad = now + (1 * 60 * 1000); + } else { + // if we haven't seen any horizontal scrolling for a while, assume + // the user might have plugged in a mousewheel + if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) { + this._likelyTrackpadUser = false; + } + } + + // don't mess with the horizontal scroll for trackpad users + // See https://github.com/vector-im/riot-web/issues/10005 + if (this._likelyTrackpadUser) { + return; + } + + if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling. // HACK: We increase the amount of scroll to counteract smooth scrolling browsers. // Smooth scrolling browsers (Firefox) use the relative area to determine the scroll // amount, which means the likely small area of content results in a small amount of diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 6c05b45111..cd752fc2ce 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -292,16 +292,6 @@ const LoggedInView = React.createClass({ const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { - case KeyCode.UP: - case KeyCode.DOWN: - if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { - const action = ev.keyCode == KeyCode.UP ? - 'view_prev_room' : 'view_next_room'; - dis.dispatch({action: action}); - handled = true; - } - break; - case KeyCode.PAGE_UP: case KeyCode.PAGE_DOWN: if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 15a244b50e..d942bb142e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -53,6 +53,8 @@ import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import DMRoomMap from '../../utils/DMRoomMap'; +import { countRoomsWithNotif } from '../../RoomNotifs'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -887,6 +889,7 @@ export default React.createClass({ } return; } + MatrixClientPeg.setJustRegisteredUserId(credentials.user_id); this.onRegistered(credentials); }, onDifferentServerClicked: (ev) => { @@ -1131,29 +1134,81 @@ export default React.createClass({ } }, + /** + * Starts a chat with the welcome user, if the user doesn't already have one + * @returns {string} The room ID of the new room, or null if no room was created + */ + async _startWelcomeUserChat() { + // We can end up with multiple tabs post-registration where the user + // might then end up with a session and we don't want them all making + // a chat with the welcome user: try to de-dupe. + // We need to wait for the first sync to complete for this to + // work though. + let waitFor; + if (!this.firstSyncComplete) { + waitFor = this.firstSyncPromise.promise; + } else { + waitFor = Promise.resolve(); + } + await waitFor; + + const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId( + this.props.config.welcomeUserId, + ); + if (welcomeUserRooms.length === 0) { + const roomId = await createRoom({ + dmUserId: this.props.config.welcomeUserId, + // Only view the welcome user if we're NOT looking at a room + andView: !this.state.currentRoomId, + spinner: false, // we're already showing one: we don't need another one + }); + // This is a bit of a hack, but since the deduplication relies + // on m.direct being up to date, we need to force a sync + // of the database, otherwise if the user goes to the other + // tab before the next save happens (a few minutes), the + // saved sync will be restored from the db and this code will + // run without the update to m.direct, making another welcome + // user room (it doesn't wait for new data from the server, just + // the saved sync to be loaded). + const saveWelcomeUser = (ev) => { + if ( + ev.getType() == 'm.direct' && + ev.getContent() && + ev.getContent()[this.props.config.welcomeUserId] + ) { + MatrixClientPeg.get().store.save(true); + MatrixClientPeg.get().removeListener( + "accountData", saveWelcomeUser, + ); + } + }; + MatrixClientPeg.get().on("accountData", saveWelcomeUser); + + return roomId; + } + return null; + }, + /** * Called when a new logged in session has started */ _onLoggedIn: async function() { this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (this._is_registered) { - this._is_registered = false; + if (MatrixClientPeg.currentUserIsJustRegistered()) { + MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { - const roomId = await createRoom({ - dmUserId: this.props.config.welcomeUserId, - // Only view the welcome user if we're NOT looking at a room - andView: !this.state.currentRoomId, - }); - // if successful, return because we're already - // viewing the welcomeUserId room - // else, if failed, fall through to view_home_page - if (roomId) { - return; + const welcomeUserRoom = await this._startWelcomeUserChat(); + if (welcomeUserRoom === null) { + // We didn't rediret to the welcome user room, so show + // the homepage. + dis.dispatch({action: 'view_home_page'}); } + } else { + // The user has just logged in after registering, + // so show the homepage. + dis.dispatch({action: 'view_home_page'}); } - // The user has just logged in after registering - dis.dispatch({action: 'view_home_page'}); } else { this._showScreenAfterLogin(); } @@ -1655,48 +1710,6 @@ export default React.createClass({ // returns a promise which resolves to the new MatrixClient onRegistered: function(credentials) { - if (this.state.register_session_id) { - // The user came in through an email validation link. To avoid overwriting - // their session, check to make sure the session isn't someone else, and - // isn't a guest user since we'll usually have set a guest user session before - // starting the registration process. This isn't perfect since it's possible - // the user had a separate guest session they didn't actually mean to replace. - const sessionOwner = Lifecycle.getStoredSessionOwner(); - const sessionIsGuest = Lifecycle.getStoredSessionIsGuest(); - if (sessionOwner && !sessionIsGuest && sessionOwner !== credentials.userId) { - console.log( - `Found a session for ${sessionOwner} but ${credentials.userId} is trying to verify their ` + - `email address. Restoring the session for ${sessionOwner} with warning.`, - ); - this._loadSession(); - - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - // N.B. first param is passed to piwik and so doesn't want i18n - Modal.createTrackedDialog('Existing session on register', '', - QuestionDialog, { - title: _t('You are logged in to another account'), - description: _t( - "Thank you for verifying your email! The account you're logged into here " + - "(%(sessionUserId)s) appears to be different from the account you've verified an " + - "email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, " + - "please log out first.", { - sessionUserId: sessionOwner, - verifiedUserId: credentials.userId, - - // TODO: Fix translations to support reusing variables. - // https://github.com/vector-im/riot-web/issues/9086 - verifiedUserId2: credentials.userId, - }, - ), - hasCancelButton: false, - }); - - return MatrixClientPeg.get(); - } - } - // XXX: This should be in state or ideally store(s) because we risk not - // rendering the most up-to-date view of state otherwise. - this._is_registered = true; return Lifecycle.setLoggedIn(credentials); }, @@ -1737,19 +1750,7 @@ export default React.createClass({ }, updateStatusIndicator: function(state, prevState) { - let notifCount = 0; - - const rooms = MatrixClientPeg.get().getRooms(); - for (let i = 0; i < rooms.length; ++i) { - if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) { - notifCount++; - } else if (rooms[i].getUnreadNotificationCount()) { - // if we were summing unread notifs: - // notifCount += rooms[i].getUnreadNotificationCount(); - // instead, we just count the number of rooms with notifs. - notifCount++; - } - } + const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count; if (PlatformPeg.get()) { PlatformPeg.get().setErrorStatus(state === 'ERROR'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 52fd6d9be4..4238e22bd9 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -108,6 +108,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this._editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -585,14 +586,14 @@ module.exports = React.createClass({ { if (resp.length > 0 && resp[0].alias) { - this.showRoomAlias(resp[0].alias); + this.showRoomAlias(resp[0].alias, true); } else { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Room not found', '', ErrorDialog, { @@ -367,13 +367,16 @@ module.exports = React.createClass({ } }, - showRoomAlias: function(alias) { - this.showRoom(null, alias); + showRoomAlias: function(alias, autoJoin=false) { + this.showRoom(null, alias, autoJoin); }, - showRoom: function(room, room_alias) { + showRoom: function(room, room_alias, autoJoin=false) { this.props.onFinished(); - const payload = {action: 'view_room'}; + const payload = { + action: 'view_room', + auto_join: autoJoin, + }; if (room) { // Don't let the user view a room they won't be able to either // peek or join: fail earlier so they don't have to click back diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cda3f60fce..03b371ef7e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1523,9 +1523,11 @@ module.exports = React.createClass({
); diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 3103ee41df..e825dd7034 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -29,6 +29,7 @@ import * as ServerType from '../../views/auth/ServerTypeSelector'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; +import MatrixClientPeg from "../../../MatrixClientPeg"; // Phases // Show controls to configure server details @@ -96,6 +97,13 @@ module.exports = React.createClass({ // Our matrix client - part of state because we can't render the UI auth // component without it. matrixClient: null, + + // The user ID we've just registered + registeredUsername: null, + + // if a different user ID to the one we just registered is logged in, + // this is the user ID that's logged in. + differentLoggedInUserId: null, }; }, @@ -167,6 +175,8 @@ module.exports = React.createClass({ _replaceClient: async function(serverConfig) { this.setState({ errorText: null, + serverDeadError: null, + serverErrorIsFatal: false, // busy while we do liveness check (we need to avoid trying to render // the UI auth component while we don't have a matrix client) busy: true, @@ -179,7 +189,10 @@ module.exports = React.createClass({ serverConfig.hsUrl, serverConfig.isUrl, ); - this.setState({serverIsAlive: true}); + this.setState({ + serverIsAlive: true, + serverErrorIsFatal: false, + }); } catch (e) { this.setState({ busy: false, @@ -287,9 +300,29 @@ module.exports = React.createClass({ return; } + MatrixClientPeg.setJustRegisteredUserId(response.user_id); + const newState = { doingUIAuth: false, + registeredUsername: response.user_id, }; + + // The user came in through an email validation link. To avoid overwriting + // their session, check to make sure the session isn't someone else, and + // isn't a guest user since we'll usually have set a guest user session before + // starting the registration process. This isn't perfect since it's possible + // the user had a separate guest session they didn't actually mean to replace. + const sessionOwner = Lifecycle.getStoredSessionOwner(); + const sessionIsGuest = Lifecycle.getStoredSessionIsGuest(); + if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) { + console.log( + `Found a session for ${sessionOwner} but ${response.userId} has just registered.`, + ); + newState.differentLoggedInUserId = sessionOwner; + } else { + newState.differentLoggedInUserId = null; + } + if (response.access_token) { const cli = await this.props.onLoggedIn({ userId: response.user_id, @@ -423,7 +456,9 @@ module.exports = React.createClass({ // If we're on a different phase, we only show the server type selector, // which is always shown if we allow custom URLs at all. - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) { + // (if there's a fatal server error, we need to show the full server + // config as the user may need to change servers to resolve the error). + if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { return
+

{_t( + "Your new account (%(newAccountId)s) is registered, but you're already " + + "logged into a different account (%(loggedInUserId)s).", { + newAccountId: this.state.registeredUsername, + loggedInUserId: this.state.differentLoggedInUserId, + }, + )}

+

+ {_t("Continue with previous account")} +

+
; + } else if (this.state.formVals.password) { // We're the client that started the registration - regDoneText = _t( + regDoneText =

{_t( "Log in to your new account.", {}, { a: (sub) => {sub}, }, - ); + )}

; } else { // We're not the original client: the user probably got to us by clicking the // email validation link. We can't offer a 'go straight to your account' link // as we don't have the original creds. - regDoneText = _t( + regDoneText =

{_t( "You can now close this window or log in to your new account.", {}, { a: (sub) => {sub}, }, - ); + )}

; } body =

{_t("Registration Successful")}

-

{ regDoneText }

+ { regDoneText }
; } else { body =
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index b3687db2bd..b52dac44a9 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -81,16 +81,10 @@ export const PasswordAuthEntry = React.createClass({ getInitialState: function() { return { - passwordValid: false, + password: "", }; }, - focus: function() { - if (this.refs.passwordField) { - this.refs.passwordField.focus(); - } - }, - _onSubmit: function(e) { e.preventDefault(); if (this.props.busy) return; @@ -98,23 +92,21 @@ export const PasswordAuthEntry = React.createClass({ this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: this.props.matrixClient.credentials.userId, - password: this.refs.passwordField.value, + password: this.state.password, }); }, _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty this.setState({ - passwordValid: Boolean(this.refs.passwordField.value), + password: ev.target.value, }); }, render: function() { - let passwordBoxClass = null; - - if (this.props.errorText) { - passwordBoxClass = 'error'; - } + const passwordBoxClass = classnames({ + "error": this.props.errorText, + }); let submitButtonOrSpinner; if (this.props.busy) { @@ -124,7 +116,7 @@ export const PasswordAuthEntry = React.createClass({ submitButtonOrSpinner = ( ); } @@ -138,17 +130,21 @@ export const PasswordAuthEntry = React.createClass({ ); } + const Field = sdk.getComponent('elements.Field'); + return (

{ _t("To continue, please enter your password.") }

-
- - +
{ submitButtonOrSpinner } diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index de4f16b684..9dcc2c9abe 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -104,6 +104,9 @@ export default class ServerConfig extends React.PureComponent { const stateForError = AutoDiscoveryUtils.authComponentStateForError(e); if (!stateForError.isFatalError) { + this.setState({ + busy: false, + }); // carry on anyway const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true); this.props.onServerConfigChange(result); diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 53cec93d03..e9f38ef27c 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component { this._onEraseFieldChange = this._onEraseFieldChange.bind(this); this.state = { - confirmButtonEnabled: false, + password: "", busy: false, shouldErase: false, errStr: null, @@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component { _onPasswordFieldChange(ev) { this.setState({ - confirmButtonEnabled: Boolean(ev.target.value), + password: ev.target.value, }); } @@ -104,7 +104,7 @@ export default class DeactivateAccountDialog extends React.Component { } const okLabel = this.state.busy ? : _t('Deactivate Account'); - const okEnabled = this.state.confirmButtonEnabled && !this.state.busy; + const okEnabled = this.state.password && !this.state.busy; let cancelButton = null; if (!this.state.busy) { @@ -113,6 +113,8 @@ export default class DeactivateAccountDialog extends React.Component { ; } + const Field = sdk.getComponent('elements.Field'); + return (

{ _t("To continue, please enter your password:") }

- {this._passwordField = e;}} className={passwordBoxClass} /> diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index da2211c10f..0720fedddc 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component { constructor(props) { super(props); + let phase = PHASE_START; + if (this.props.verifier.cancelled) { + console.log("Verifier was cancelled in the background."); + phase = PHASE_CANCELLED; + } + this._showSasEvent = null; this.state = { - phase: PHASE_START, + phase: phase, sasVerified: false, opponentProfile: null, opponentProfileError: null, diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js new file mode 100644 index 0000000000..9d533eab56 --- /dev/null +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -0,0 +1,108 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import { _t } from '../../../languageHandler'; +import sdk from "../../../index"; +import {wantsDateSeparator} from '../../../DateUtils'; +import SettingsStore from '../../../settings/SettingsStore'; + +export default class MessageEditHistoryDialog extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + events: [], + nextBatch: null, + isLoading: true, + isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), + }; + } + + loadMoreEdits = async (backwards) => { + if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { + // bail out on backwards as we only paginate in one direction + return false; + } + const opts = {from: this.state.nextBatch}; + const roomId = this.props.mxEvent.getRoomId(); + const eventId = this.props.mxEvent.getId(); + const result = await MatrixClientPeg.get().relations( + roomId, eventId, "m.replace", "m.room.message", opts); + let resolve; + const promise = new Promise(r => resolve = r); + this.setState({ + events: this.state.events.concat(result.events), + nextBatch: result.nextBatch, + isLoading: false, + }, () => { + const hasMoreResults = !!this.state.nextBatch; + resolve(hasMoreResults); + }); + return promise; + } + + componentDidMount() { + this.loadMoreEdits(); + } + + _renderEdits() { + const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage'); + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const nodes = []; + let lastEvent; + this.state.events.forEach(e => { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + nodes.push(
  • ); + } + nodes.push(); + lastEvent = e; + }); + return nodes; + } + + render() { + let content; + if (this.state.error) { + content = this.state.error; + } else if (this.state.isLoading) { + const Spinner = sdk.getComponent("elements.Spinner"); + content = ; + } else { + const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + content = ( +
      {this._renderEdits()}
    +
    ); + } + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + + {content} + + ); + } +} diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index ce8b93f693..45c242fea5 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -92,7 +92,7 @@ export default React.createClass({

    {_t( "Upgrading this room requires closing down the current " + - "instance of the room and creating a new room it its place. " + + "instance of the room and creating a new room in its place. " + "To give room members the best possible experience, we will:", )}

    diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.js index e7b22950d6..98c031e89b 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.js +++ b/src/components/views/dialogs/UploadConfirmDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import filesize from "filesize"; export default class UploadConfirmDialog extends React.Component { static propTypes = { @@ -49,6 +51,10 @@ export default class UploadConfirmDialog extends React.Component { this.props.onFinished(true); } + _onUploadAllClick = () => { + this.props.onFinished(true, true); + } + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -71,7 +77,7 @@ export default class UploadConfirmDialog extends React.Component { preview =
    -
    {this.props.file.name}
    +
    {this.props.file.name} ({filesize(this.props.file.size)})
    ; } else { @@ -80,11 +86,18 @@ export default class UploadConfirmDialog extends React.Component { - {this.props.file.name} + {this.props.file.name} ({filesize(this.props.file.size)})
    ; } + let uploadAllButton; + if (this.props.currentIndex + 1 < this.props.totalFiles) { + uploadAllButton = ; + } + return ( + > + {uploadAllButton} + ); } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 959cee7ace..034a3318a5 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -240,19 +240,13 @@ export default class AppTile extends React.Component { if (this.props.onEditClick) { this.props.onEditClick(); } else { + // The dialog handles scalar auth for us const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this._scalarClient.connect().done(() => { - const src = this._scalarClient.getScalarInterfaceUrlForRoom( - this.props.room, 'type_' + this.props.type, this.props.id); - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, "mx_IntegrationsManager"); - }, (err) => { - this.setState({ - error: err.message, - }); - console.error('Error ensuring a valid scalar_token exists', err); - }); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: 'type_' + this.props.type, + integrationId: this.props.id, + }, "mx_IntegrationsManager"); } } diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js new file mode 100644 index 0000000000..52d51e0b39 --- /dev/null +++ b/src/components/views/elements/InteractiveTooltip.js @@ -0,0 +1,195 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container"; + +// If the distance from tooltip to window edge is below this value, the tooltip +// will flip around to the other side of the target. +const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20; + +function getOrCreateContainer() { + let container = document.getElementById(InteractiveTooltipContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = InteractiveTooltipContainerId; + document.body.appendChild(container); + } + + return container; +} + +function isInRect(x, y, rect, buffer = 10) { + const { top, right, bottom, left } = rect; + return x >= (left - buffer) && x <= (right + buffer) + && y >= (top - buffer) && y <= (bottom + buffer); +} + +/* + * This style of tooltip takes a "target" element as its child and centers the + * tooltip along one edge of the target. + */ +export default class InteractiveTooltip extends React.Component { + propTypes: { + // Content to show in the tooltip + content: PropTypes.node.isRequired, + // Function to call when visibility of the tooltip changes + onVisibilityChange: PropTypes.func, + }; + + constructor() { + super(); + + this.state = { + contentRect: null, + visible: false, + }; + } + + componentDidUpdate() { + // Whenever this passthrough component updates, also render the tooltip + // in a separate DOM tree. This allows the tooltip content to participate + // the normal React rendering cycle: when this component re-renders, the + // tooltip content re-renders. + // Once we upgrade to React 16, this could be done a bit more naturally + // using the portals feature instead. + this.renderTooltip(); + } + + collectContentRect = (element) => { + // We don't need to clean up when unmounting, so ignore + if (!element) return; + + this.setState({ + contentRect: element.getBoundingClientRect(), + }); + } + + collectTarget = (element) => { + this.target = element; + } + + onBackgroundClick = (ev) => { + this.hideTooltip(); + } + + onBackgroundMouseMove = (ev) => { + const { clientX: x, clientY: y } = ev; + const { contentRect } = this.state; + const targetRect = this.target.getBoundingClientRect(); + + if (!isInRect(x, y, contentRect) && !isInRect(x, y, targetRect)) { + this.hideTooltip(); + return; + } + } + + onTargetMouseOver = (ev) => { + this.showTooltip(); + } + + showTooltip() { + this.setState({ + visible: true, + }); + if (this.props.onVisibilityChange) { + this.props.onVisibilityChange(true); + } + } + + hideTooltip() { + this.setState({ + visible: false, + }); + if (this.props.onVisibilityChange) { + this.props.onVisibilityChange(false); + } + } + + renderTooltip() { + const { contentRect, visible } = this.state; + if (!visible) { + ReactDOM.unmountComponentAtNode(getOrCreateContainer()); + return null; + } + + const targetRect = this.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const targetLeft = targetRect.left + window.pageXOffset; + const targetBottom = targetRect.bottom + window.pageYOffset; + const targetTop = targetRect.top + window.pageYOffset; + + // Place the tooltip above the target by default. If we find that the + // tooltip content would extend past the safe area towards the window + // edge, flip around to below the target. + const position = {}; + let chevronFace = null; + if (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) { + position.top = targetBottom; + chevronFace = "top"; + } else { + position.bottom = window.innerHeight - targetTop; + chevronFace = "bottom"; + } + + // Center the tooltip horizontally with the target's center. + position.left = targetLeft + targetRect.width / 2; + + const chevron =
    ; + + const menuClasses = classNames({ + 'mx_InteractiveTooltip': true, + 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', + 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', + }); + + const menuStyle = {}; + if (contentRect) { + menuStyle.left = `-${contentRect.width / 2}px`; + } + + const tooltip =
    +
    +
    + {chevron} + {this.props.content} +
    +
    ; + + ReactDOM.render(tooltip, getOrCreateContainer()); + } + + render() { + // We use `cloneElement` here to append some props to the child content + // without using a wrapper element which could disrupt layout. + return React.cloneElement(this.props.children, { + ref: this.collectTarget, + onMouseOver: this.onTargetMouseOver, + }); + } +} diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index 165cd20eb5..ef5604dba6 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,95 +18,34 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import classNames from 'classnames'; -import SdkConfig from '../../../SdkConfig'; import ScalarAuthClient from '../../../ScalarAuthClient'; -import ScalarMessaging from '../../../ScalarMessaging'; import Modal from "../../../Modal"; import { _t } from '../../../languageHandler'; -import AccessibleButton from './AccessibleButton'; export default class ManageIntegsButton extends React.Component { constructor(props) { super(props); - - this.state = { - scalarError: null, - }; - - this.onManageIntegrations = this.onManageIntegrations.bind(this); } - componentWillMount() { - ScalarMessaging.startListening(); - this.scalarClient = null; - - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().done(() => { - this.forceUpdate(); - }, (err) => { - this.setState({scalarError: err}); - console.error('Error whilst initialising scalarClient for ManageIntegsButton', err); - }); - } - } - - componentWillUnmount() { - ScalarMessaging.stopListening(); - } - - onManageIntegrations(ev) { + onManageIntegrations = (ev) => { ev.preventDefault(); - if (this.state.scalarError && !this.scalarClient.hasCredentials()) { - return; - } + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this.scalarClient.connect().done(() => { - Modal.createDialog(IntegrationsManager, { - src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) : - null, - }, "mx_IntegrationsManager"); - }, (err) => { - this.setState({scalarError: err}); - console.error('Error ensuring a valid scalar_token exists', err); - }); - } + Modal.createDialog(IntegrationsManager, { + room: this.props.room, + }, "mx_IntegrationsManager"); + }; render() { let integrationsButton =
    ; - let integrationsWarningTriangle =
    ; - let integrationsErrorPopup =
    ; - if (this.scalarClient !== null) { - const integrationsButtonClasses = classNames({ - mx_RoomHeader_button: true, - mx_RoomHeader_manageIntegsButton: true, - mx_ManageIntegsButton_error: !!this.state.scalarError, - }); - - if (this.state.scalarError && !this.scalarClient.hasCredentials()) { - integrationsWarningTriangle = ; - // Popup shown when hovering over integrationsButton_error (via CSS) - integrationsErrorPopup = ( - - { _t('Could not connect to the integration server') } - - ); - } - + if (ScalarAuthClient.isPossible()) { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( - - { integrationsWarningTriangle } - { integrationsErrorPopup } - + /> ); } diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 72091f2e1c..ce22bf2fc6 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -78,6 +78,14 @@ export default class MessageEditor extends React.Component { this.model.update(text, event.inputType, caret); } + _insertText(textToInsert, inputType = "insertText") { + const sel = document.getSelection(); + const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); + const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); + caret.offset += textToInsert.length; + this.model.update(newText, inputType, caret); + } + _isCaretAtStart() { const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection()); return caret.offset === 0; @@ -92,7 +100,7 @@ export default class MessageEditor extends React.Component { // insert newline on Shift+Enter if (event.shiftKey && event.key === "Enter") { event.preventDefault(); // just in case the browser does support this - document.execCommand("insertHTML", undefined, "\n"); + this._insertText("\n"); return; } // autocomplete or enter to send below shouldn't have any modifier keys pressed. @@ -150,16 +158,28 @@ export default class MessageEditor extends React.Component { dis.dispatch({action: 'focus_composer'}); } + _isEmote() { + const firstPart = this.model.parts[0]; + return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); + } + _sendEdit = () => { + const isEmote = this._isEmote(); + let model = this.model; + if (isEmote) { + // trim "/me " + model = model.clone(); + model.removeText({index: 0, offset: 0}, 4); + } const newContent = { - "msgtype": "m.text", - "body": textSerialize(this.model), + "msgtype": isEmote ? "m.emote" : "m.text", + "body": textSerialize(model), }; const contentBody = { msgtype: newContent.msgtype, body: ` * ${newContent.body}`, }; - const formattedBody = htmlSerializeIfNeeded(this.model); + const formattedBody = htmlSerializeIfNeeded(model); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; @@ -232,7 +252,7 @@ export default class MessageEditor extends React.Component { parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { // otherwise, parse the body of the event - parts = parseEvent(editState.getEvent(), room, this.context.matrixClient); + parts = parseEvent(editState.getEvent(), partCreator); } return new EditorModel( diff --git a/src/components/views/elements/ToolTipButton.js b/src/components/views/elements/TooltipButton.js similarity index 83% rename from src/components/views/elements/ToolTipButton.js rename to src/components/views/elements/TooltipButton.js index 239095f196..63cf3fe1fe 100644 --- a/src/components/views/elements/ToolTipButton.js +++ b/src/components/views/elements/TooltipButton.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,7 @@ import React from 'react'; import sdk from '../../../index'; module.exports = React.createClass({ - displayName: 'ToolTipButton', + displayName: 'TooltipButton', getInitialState: function() { return { @@ -41,12 +42,12 @@ module.exports = React.createClass({ render: function() { const Tooltip = sdk.getComponent("elements.Tooltip"); const tip = this.state.hover ? :
    ; return ( -
    +
    ? { tip }
    diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index db060218d4..7296b25344 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -224,7 +224,7 @@ module.exports = React.createClass({
    - { this.state.groupRoom.canonical_alias } + { this.state.groupRoom.canonicalAlias }
    diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js new file mode 100644 index 0000000000..fef9c362c6 --- /dev/null +++ b/src/components/views/messages/EditHistoryMessage.js @@ -0,0 +1,61 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import * as HtmlUtils from '../../../HtmlUtils'; +import {formatTime} from '../../../DateUtils'; +import {MatrixEvent} from 'matrix-js-sdk'; +import {pillifyLinks} from '../../../utils/pillify'; + +export default class EditHistoryMessage extends React.PureComponent { + static propTypes = { + // the message event being edited + mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, + }; + + componentDidMount() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + componentDidUpdate() { + pillifyLinks(this.refs.content.children, this.props.mxEvent); + } + + render() { + const {mxEvent} = this.props; + const originalContent = mxEvent.getOriginalContent(); + const content = originalContent["m.new_content"] || originalContent; + const contentElements = HtmlUtils.bodyToHtml(content); + let contentContainer; + if (mxEvent.getContent().msgtype === "m.emote") { + const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + contentContainer = (
    *  + { name } +  {contentElements} +
    ); + } else { + contentContainer = (
    {contentElements}
    ); + } + const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour); + return
  • +
    + {timestamp} + { contentContainer } +
    +
  • ; + } +} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 80f0ba538c..e7843c1505 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -57,7 +57,7 @@ export default class MessageActionBar extends React.PureComponent { this.props.onFocusChange(focused); } - onCryptoClicked = () => { + onCryptoClick = () => { const event = this.props.mxEvent; Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', import('../../../async-components/views/dialogs/EncryptedEventDialog'), @@ -89,7 +89,7 @@ export default class MessageActionBar extends React.PureComponent { let e2eInfoCallback = null; if (this.props.mxEvent.isEncrypted()) { - e2eInfoCallback = () => this.onCryptoClicked(); + e2eInfoCallback = () => this.onCryptoClick(); } const menuOptions = { @@ -131,43 +131,28 @@ export default class MessageActionBar extends React.PureComponent { return SettingsStore.isFeatureEnabled("feature_message_editing"); } - renderAgreeDimension() { + renderReactButton() { if (!this.isReactionsEnabled()) { return null; } - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; - } + const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction'); + const { mxEvent, reactions } = this.props; - renderLikeDimension() { - if (!this.isReactionsEnabled()) { - return null; - } - - const ReactionDimension = sdk.getComponent('messages.ReactionDimension'); - return ; } render() { - let agreeDimensionReactionButtons; - let likeDimensionReactionButtons; + let reactButton; let replyButton; let editButton; if (isContentActionable(this.props.mxEvent)) { - agreeDimensionReactionButtons = this.renderAgreeDimension(); - likeDimensionReactionButtons = this.renderLikeDimension(); + reactButton = this.renderReactButton(); replyButton = - {agreeDimensionReactionButtons} - {likeDimensionReactionButtons} + {reactButton} {replyButton} {editButton} { + if (!this.props.onFocusChange) { + return; + } + this.props.onFocusChange(focused); + } + + componentDidUpdate(prevProps) { + if (prevProps.reactions !== this.props.reactions) { + this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); + this.props.reactions.on("Relations.redaction", this.onReactionsChange); + this.onReactionsChange(); + } + } + + componentWillUnmount() { + if (this.props.reactions) { + this.props.reactions.removeListener( + "Relations.add", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.redaction", + this.onReactionsChange, + ); + } + } + + onReactionsChange = () => { + // Force a re-render of the tooltip because a change in the reactions + // set means the event tile's layout may have changed and possibly + // altered the location where the tooltip should be shown. + this.forceUpdate(); + } + + render() { + const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip'); + const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); + const { mxEvent, reactions } = this.props; + + const content = ; + + return + + ; + } +} diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js deleted file mode 100644 index de33ad1a57..0000000000 --- a/src/components/views/messages/ReactionDimension.js +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import MatrixClientPeg from '../../../MatrixClientPeg'; - -export default class ReactionDimension extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // Array of strings containing the emoji for each option - options: PropTypes.array.isRequired, - title: PropTypes.string, - // The Relations model from the JS SDK for reactions - reactions: PropTypes.object, - }; - - constructor(props) { - super(props); - - this.state = this.getSelection(); - - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.reactions !== this.props.reactions) { - this.props.reactions.on("Relations.add", this.onReactionsChange); - this.props.reactions.on("Relations.remove", this.onReactionsChange); - this.props.reactions.on("Relations.redaction", this.onReactionsChange); - this.onReactionsChange(); - } - } - - componentWillUnmount() { - if (this.props.reactions) { - this.props.reactions.removeListener( - "Relations.add", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.remove", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.redaction", - this.onReactionsChange, - ); - } - } - - onReactionsChange = () => { - this.setState(this.getSelection()); - } - - getSelection() { - const myReactions = this.getMyReactions(); - if (!myReactions) { - return { - selectedOption: null, - selectedReactionEvent: null, - }; - } - const { options } = this.props; - let selectedOption = null; - let selectedReactionEvent = null; - for (const option of options) { - const reactionForOption = myReactions.find(mxEvent => { - if (mxEvent.isRedacted()) { - return false; - } - return mxEvent.getRelation().key === option; - }); - if (!reactionForOption) { - continue; - } - if (selectedOption) { - // If there are multiple selected values (only expected to occur via - // non-Riot clients), then act as if none are selected. - return { - selectedOption: null, - selectedReactionEvent: null, - }; - } - selectedOption = option; - selectedReactionEvent = reactionForOption; - } - return { selectedOption, selectedReactionEvent }; - } - - getMyReactions() { - const reactions = this.props.reactions; - if (!reactions) { - return null; - } - const userId = MatrixClientPeg.get().getUserId(); - const myReactions = reactions.getAnnotationsBySender()[userId]; - if (!myReactions) { - return null; - } - return [...myReactions.values()]; - } - - onOptionClick = (ev) => { - const { key } = ev.target.dataset; - this.toggleDimension(key); - } - - toggleDimension(key) { - const { selectedOption, selectedReactionEvent } = this.state; - const newSelectedOption = selectedOption !== key ? key : null; - this.setState({ - selectedOption: newSelectedOption, - }); - if (selectedReactionEvent) { - MatrixClientPeg.get().redactEvent( - this.props.mxEvent.getRoomId(), - selectedReactionEvent.getId(), - ); - } - if (newSelectedOption) { - MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { - "m.relates_to": { - "rel_type": "m.annotation", - "event_id": this.props.mxEvent.getId(), - "key": newSelectedOption, - }, - }); - } - } - - render() { - const { selectedOption } = this.state; - const { options } = this.props; - - const items = options.map(option => { - const disabled = selectedOption && selectedOption !== option; - const classes = classNames({ - mx_ReactionDimension_disabled: disabled, - }); - return - {option} - ; - }); - - return - {items} - ; - } -} diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js new file mode 100644 index 0000000000..e09b9ade69 --- /dev/null +++ b/src/components/views/messages/ReactionTooltipButton.js @@ -0,0 +1,68 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import MatrixClientPeg from '../../../MatrixClientPeg'; + +export default class ReactionTooltipButton extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji + content: PropTypes.string.isRequired, + title: PropTypes.string, + // A possible Matrix event if the current user has voted for this type + myReactionEvent: PropTypes.object, + }; + + onClick = (ev) => { + const { mxEvent, myReactionEvent, content } = this.props; + if (myReactionEvent) { + MatrixClientPeg.get().redactEvent( + mxEvent.getRoomId(), + myReactionEvent.getId(), + ); + } else { + MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": mxEvent.getId(), + "key": content, + }, + }); + } + } + + render() { + const { content, myReactionEvent } = this.props; + + const classes = classNames({ + mx_ReactionTooltipButton: true, + mx_ReactionTooltipButton_selected: !!myReactionEvent, + }); + + return + {content} + ; + } +} diff --git a/src/components/views/messages/ReactionsQuickTooltip.js b/src/components/views/messages/ReactionsQuickTooltip.js new file mode 100644 index 0000000000..0505bbd2df --- /dev/null +++ b/src/components/views/messages/ReactionsQuickTooltip.js @@ -0,0 +1,195 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { unicodeToShortcode } from '../../../HtmlUtils'; + +export default class ReactionsQuickTooltip extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + // The Relations model from the JS SDK for reactions to `mxEvent` + reactions: PropTypes.object, + }; + + constructor(props) { + super(props); + + if (props.reactions) { + props.reactions.on("Relations.add", this.onReactionsChange); + props.reactions.on("Relations.remove", this.onReactionsChange); + props.reactions.on("Relations.redaction", this.onReactionsChange); + } + + this.state = { + hoveredItem: null, + myReactions: this.getMyReactions(), + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.reactions !== this.props.reactions) { + this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); + this.props.reactions.on("Relations.redaction", this.onReactionsChange); + this.onReactionsChange(); + } + } + + componentWillUnmount() { + if (this.props.reactions) { + this.props.reactions.removeListener( + "Relations.add", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.redaction", + this.onReactionsChange, + ); + } + } + + onReactionsChange = () => { + this.setState({ + myReactions: this.getMyReactions(), + }); + } + + getMyReactions() { + const reactions = this.props.reactions; + if (!reactions) { + return null; + } + const userId = MatrixClientPeg.get().getUserId(); + const myReactions = reactions.getAnnotationsBySender()[userId]; + if (!myReactions) { + return null; + } + return [...myReactions.values()]; + } + + onMouseOver = (ev) => { + const { key } = ev.target.dataset; + const item = this.items.find(({ content }) => content === key); + this.setState({ + hoveredItem: item, + }); + } + + onMouseOut = (ev) => { + this.setState({ + hoveredItem: null, + }); + } + + get items() { + return [ + { + content: "👍", + title: _t("Agree"), + }, + { + content: "👎", + title: _t("Disagree"), + }, + { + content: "😄", + title: _t("Happy"), + }, + { + content: "🎉", + title: _t("Party Popper"), + }, + { + content: "😕", + title: _t("Confused"), + }, + { + content: "❤️", + title: _t("Heart"), + }, + { + content: "🚀", + title: _t("Rocket"), + }, + { + content: "👀", + title: _t("Eyes"), + }, + ]; + } + + render() { + const { mxEvent } = this.props; + const { myReactions, hoveredItem } = this.state; + const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton'); + + const buttons = this.items.map(({ content, title }) => { + const myReactionEvent = myReactions && myReactions.find(mxEvent => { + if (mxEvent.isRedacted()) { + return false; + } + return mxEvent.getRelation().key === content; + }); + + return ; + }); + + let label = " "; // non-breaking space to keep layout the same when empty + if (hoveredItem) { + const { content, title } = hoveredItem; + + let shortcodeLabel; + const shortcode = unicodeToShortcode(content); + if (shortcode) { + shortcodeLabel = + {shortcode} + ; + } + + label =
    + + {title} + + {shortcodeLabel} +
    ; + } + + return
    +
    + {buttons} +
    + {label} +
    ; + } +} diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index 51f62807a5..57d2afc429 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -18,10 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; import { isContentActionable } from '../../../utils/EventUtils'; import { isSingleEmoji } from '../../../HtmlUtils'; import MatrixClientPeg from '../../../MatrixClientPeg'; +// The maximum number of reactions to initially show on a message. +const MAX_ITEMS_WHEN_LIMITED = 8; + export default class ReactionsRow extends React.PureComponent { static propTypes = { // The event we're displaying reactions for @@ -41,6 +45,7 @@ export default class ReactionsRow extends React.PureComponent { this.state = { myReactions: this.getMyReactions(), + showAll: false, }; } @@ -94,16 +99,22 @@ export default class ReactionsRow extends React.PureComponent { return [...myReactions.values()]; } + onShowAllClick = () => { + this.setState({ + showAll: true, + }); + } + render() { const { mxEvent, reactions } = this.props; - const { myReactions } = this.state; + const { myReactions, showAll } = this.state; if (!reactions || !isContentActionable(mxEvent)) { return null; } const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton'); - const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { + let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { if (!isSingleEmoji(content)) { return null; } @@ -125,10 +136,26 @@ export default class ReactionsRow extends React.PureComponent { reactionEvents={events} myReactionEvent={myReactionEvent} />; - }); + }).filter(item => !!item); + + // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. + // The "+ 1" ensure that the "show all" reveals something that takes up + // more space than the button itself. + let showAllButton; + if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) { + items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); + showAllButton = + {_t("Show all")} + ; + } return
    {items} + {showAllButton}
    ; } } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 6f480b8d3c..25316844df 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -30,12 +30,11 @@ import Modal from '../../../Modal'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import * as ContextualMenu from '../../structures/ContextualMenu'; import SettingsStore from "../../../settings/SettingsStore"; -import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import ReplyThread from "../elements/ReplyThread"; import {host as matrixtoHost} from '../../../matrix-to'; +import {pillifyLinks} from '../../../utils/pillify'; module.exports = React.createClass({ displayName: 'TextualBody', @@ -99,7 +98,7 @@ module.exports = React.createClass({ // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. - this.pillifyLinks(this.refs.content.children); + pillifyLinks(this.refs.content.children, this.props.mxEvent); HtmlUtils.linkifyElement(this.refs.content); this.calculateUrlPreview(); @@ -184,98 +183,6 @@ module.exports = React.createClass({ } }, - pillifyLinks: function(nodes) { - const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); - let node = nodes[0]; - while (node) { - let pillified = false; - - if (node.tagName === "A" && node.getAttribute("href")) { - const href = node.getAttribute("href"); - - // If the link is a (localised) matrix.to link, replace it with a pill - const Pill = sdk.getComponent('elements.Pill'); - if (Pill.isMessagePillUrl(href)) { - const pillContainer = document.createElement('span'); - - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pill = ; - - ReactDOM.render(pill, pillContainer); - node.parentNode.replaceChild(pillContainer, node); - // Pills within pills aren't going to go well, so move on - pillified = true; - - // update the current node with one that's now taken its place - node = pillContainer; - } - } else if (node.nodeType === Node.TEXT_NODE) { - const Pill = sdk.getComponent('elements.Pill'); - - let currentTextNode = node; - const roomNotifTextNodes = []; - - // Take a textNode and break it up to make all the instances of @room their - // own textNode, adding those nodes to roomNotifTextNodes - while (currentTextNode !== null) { - const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); - let nextTextNode = null; - if (roomNotifPos > -1) { - let roomTextNode = currentTextNode; - - if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); - if (roomTextNode.textContent.length > Pill.roomNotifLen()) { - nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); - } - roomNotifTextNodes.push(roomTextNode); - } - currentTextNode = nextTextNode; - } - - if (roomNotifTextNodes.length > 0) { - const pushProcessor = new PushProcessor(MatrixClientPeg.get()); - const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); - if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { - // Now replace all those nodes with Pills - for (const roomNotifTextNode of roomNotifTextNodes) { - // Set the next node to be processed to the one after the node - // we're adding now, since we've just inserted nodes into the structure - // we're iterating over. - // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once - node = roomNotifTextNode.nextSibling; - - const pillContainer = document.createElement('span'); - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pill = ; - - ReactDOM.render(pill, pillContainer); - roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); - } - // Nothing else to do for a text node (and we don't need to advance - // the loop pointer because we did it above) - continue; - } - } - } - - if (node.childNodes && node.childNodes.length && !pillified) { - this.pillifyLinks(node.childNodes); - } - - node = node.nextSibling; - } - }, - findLinks: function(nodes) { let links = []; @@ -448,6 +355,11 @@ module.exports = React.createClass({ this.setState({editedMarkerHovered: false}); }, + _openHistoryDialog: async function() { + const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog"); + Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent}); + }, + _renderEditedMarker: function() { let editedTooltip; if (this.state.editedMarkerHovered) { @@ -456,12 +368,13 @@ module.exports = React.createClass({ const date = editEvent && formatDate(editEvent.getDate()); editedTooltip = ; } return (
    {editedTooltip}{`(${_t("edited")})`}
    diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index e0e7a48b8c..3e5528996f 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile'; import Modal from '../../../Modal'; import dis from '../../../dispatcher'; import sdk from '../../../index'; -import SdkConfig from '../../../SdkConfig'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarMessaging from '../../../ScalarMessaging'; import { _t } from '../../../languageHandler'; import WidgetUtils from '../../../utils/WidgetUtils'; @@ -63,20 +61,6 @@ module.exports = React.createClass({ }, componentDidMount: function() { - this.scalarClient = null; - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().then(() => { - this.forceUpdate(); - }).catch((e) => { - console.log('Failed to connect to integrations server'); - // TODO -- Handle Scalar errors - // this.setState({ - // scalar_error: err, - // }); - }); - } - this.dispatcherRef = dis.register(this.onAction); }, @@ -144,16 +128,10 @@ module.exports = React.createClass({ _launchManageIntegrations: function() { const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager'); - this.scalarClient.connect().done(() => { - const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') : - null; - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, 'mx_IntegrationsManager'); - }, (err) => { - console.error('Error ensuring a valid scalar_token exists', err); - }); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: 'add_integ', + }, 'mx_IntegrationsManager'); }, onClickAddWidget: function(e) { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9837b4a029..988bf7eb3c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -403,7 +404,7 @@ module.exports = withMatrixClient(React.createClass({ }); }, - onCryptoClicked: function(e) { + onCryptoClick: function(e) { const event = this.props.mxEvent; Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', @@ -439,7 +440,7 @@ module.exports = withMatrixClient(React.createClass({ _renderE2EPadlock: function() { const ev = this.props.mxEvent; - const props = {onClick: this.onCryptoClicked}; + const props = {onClick: this.onCryptoClick}; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { @@ -670,13 +671,13 @@ module.exports = withMatrixClient(React.createClass({ {'requestLink': (sub) => { sub }}, ); - const ToolTipButton = sdk.getComponent('elements.ToolTipButton'); + const TooltipButton = sdk.getComponent('elements.TooltipButton'); const keyRequestInfo = isEncryptionFailure ?
    { keyRequestInfoContent } - +
    : null; let reactionsRow; @@ -828,7 +829,7 @@ module.exports.haveTileForEvent = function(e) { if (e.isRedacted() && !isMessageEvent(e)) return false; // No tile for replacement events since they update the original tile - if (e.isRelation("m.replace")) return false; + if (e.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return false; const handler = getHandlerTile(e); if (handler === undefined) return false; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e99e87ae1d..7a64e9ad51 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; import {findEditableEvent} from '../../../utils/EventUtils'; +import ComposerHistoryManager from "../../../ComposerHistoryManager"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; + historyManager: ComposerHistoryManager; constructor(props, context) { super(props, context); @@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = dis.register(this.onAction); + this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { @@ -679,14 +682,6 @@ export default class MessageComposerInput extends React.Component { if (this.autocomplete.countCompletions() > 0) { if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) { switch (ev.keyCode) { - case KeyCode.LEFT: - this.autocomplete.moveSelection(-1); - ev.preventDefault(); - return true; - case KeyCode.RIGHT: - this.autocomplete.moveSelection(+1); - ev.preventDefault(); - return true; case KeyCode.UP: this.autocomplete.moveSelection(-1); ev.preventDefault(); @@ -1062,6 +1057,7 @@ export default class MessageComposerInput extends React.Component { if (cmd) { if (!cmd.error) { + this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); this.setState({ editorState: this.createEditorState(), }, ()=>{ @@ -1139,6 +1135,8 @@ export default class MessageComposerInput extends React.Component { let sendHtmlFn = ContentHelpers.makeHtmlMessage; let sendTextFn = ContentHelpers.makeTextMessage; + this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); + if (commandText && commandText.startsWith('/me')) { if (replyingToEv) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -1198,19 +1196,31 @@ export default class MessageComposerInput extends React.Component { }; onVerticalArrow = (e, up) => { - if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return; - - // Select history - const selection = this.state.editorState.selection; + if (e.ctrlKey || e.shiftKey || e.metaKey) return; // selection must be collapsed + const selection = this.state.editorState.selection; if (!selection.isCollapsed) return; - const document = this.state.editorState.document; - // and we must be at the edge of the document (up=start, down=end) + const document = this.state.editorState.document; if (up) { if (!selection.anchor.isAtStartOfNode(document)) return; + } else { + if (!selection.anchor.isAtEndOfNode(document)) return; + } + const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); + const shouldSelectHistory = (editingEnabled && e.altKey) || !editingEnabled; + const shouldEditLastMessage = editingEnabled && !e.altKey && up; + + if (shouldSelectHistory) { + // Try select composer history + const selected = this.selectHistory(up); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + } + } else if (shouldEditLastMessage) { const editEvent = findEditableEvent(this.props.room, false); if (editEvent) { // We're selecting history, so prevent the key event from doing anything else @@ -1223,6 +1233,54 @@ export default class MessageComposerInput extends React.Component { } }; + selectHistory = (up) => { + const delta = up ? -1 : 1; + + // True if we are not currently selecting history, but composing a message + if (this.historyManager.currentIndex === this.historyManager.history.length) { + // We can't go any further - there isn't any more history, so nop. + if (!up) { + return; + } + this.setState({ + currentlyComposedEditorState: this.state.editorState, + }); + } else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) { + // True when we return to the message being composed currently + this.setState({ + editorState: this.state.currentlyComposedEditorState, + }); + this.historyManager.currentIndex = this.historyManager.history.length; + return; + } + + let editorState; + const historyItem = this.historyManager.getItem(delta); + if (!historyItem) return; + + if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) { + editorState = this.richToMdEditorState(historyItem.value); + } else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) { + editorState = this.mdToRichEditorState(historyItem.value); + } else { + editorState = historyItem.value; + } + + // Move selection to the end of the selected history + const change = editorState.change().moveToEndOfNode(editorState.document); + + // We don't call this.onChange(change) now, as fixups on stuff like pills + // should already have been done and persisted in the history. + editorState = change.value; + + this.suppressAutoComplete = true; + + this.setState({ editorState }, ()=>{ + this._editor.focus(); + }); + return true; + }; + onTab = async (e) => { this.setState({ someCompletions: null, diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index cbc44d0933..fb5bc3ae0d 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -66,6 +66,7 @@ module.exports = React.createClass({ error: PropTypes.object, canPreview: PropTypes.bool, + previewLoading: PropTypes.bool, room: PropTypes.object, // When a spinner is present, a spinnerState can be specified to indicate the @@ -254,6 +255,8 @@ module.exports = React.createClass({ }, render: function() { + const Spinner = sdk.getComponent('elements.Spinner'); + let showSpinner = false; let darkStyle = false; let title; @@ -262,6 +265,7 @@ module.exports = React.createClass({ let primaryActionLabel; let secondaryActionHandler; let secondaryActionLabel; + let footer; const messageCase = this._getMessageCase(); switch (messageCase) { @@ -287,6 +291,14 @@ module.exports = React.createClass({ primaryActionHandler = this.onRegisterClick; secondaryActionLabel = _t("Sign In"); secondaryActionHandler = this.onLoginClick; + if (this.props.previewLoading) { + footer = ( +
    + + {_t("Loading room preview")} +
    + ); + } break; } case MessageCase.Kicked: { @@ -433,7 +445,6 @@ module.exports = React.createClass({ } const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const Spinner = sdk.getComponent('elements.Spinner'); let subTitleElements; if (subTitle) { @@ -484,6 +495,9 @@ module.exports = React.createClass({ { secondaryButton } { primaryButton }
    +
    + { footer } +
    ); }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e1b9567ebd..be73985d16 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -67,14 +68,6 @@ module.exports = React.createClass({ }); }, - _shouldShowNotifBadge: function() { - return RoomNotifs.BADGE_STATES.includes(this.state.notifState); - }, - - _shouldShowMentionBadge: function() { - return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState); - }, - _isDirectMessageRoom: function(roomId) { const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId); return Boolean(dmRooms); @@ -301,8 +294,8 @@ module.exports = React.createClass({ const notificationCount = this.props.notificationCount; // var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); - const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); - const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); + const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState); + const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState); const badges = notifBadges || mentionBadges; let subtext = null; diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index c2e2ba89d4..edde0a6865 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -97,20 +97,22 @@ module.exports = React.createClass({ return (
    -
    - {_t( - "This room is running room version , which this homeserver has " + - "marked as unstable.", - {}, - { - "roomVersion": () => {this.props.room.getVersion()}, - "i": (sub) => {sub}, - }, - )} -
    - {doUpgradeWarnings} -
    - {_t("Only room administrators will see this warning")} +
    +
    + {_t( + "This room is running room version , which this homeserver has " + + "marked as unstable.", + {}, + { + "roomVersion": () => {this.props.room.getVersion()}, + "i": (sub) => {sub}, + }, + )} +
    + {doUpgradeWarnings} +
    + {_t("Only room administrators will see this warning")} +
    ); diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index a0e3f1b7a9..6918810842 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import { _t } from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import sdk from '../../../index'; -import SdkConfig from '../../../SdkConfig'; import ScalarAuthClient from '../../../ScalarAuthClient'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; @@ -53,6 +52,9 @@ export default class Stickerpicker extends React.Component { this.popoverWidth = 300; this.popoverHeight = 300; + // This is loaded by _acquireScalarClient on an as-needed basis. + this.scalarClient = null; + this.state = { showStickers: false, imError: null, @@ -63,14 +65,34 @@ export default class Stickerpicker extends React.Component { }; } - _removeStickerpickerWidgets() { + _acquireScalarClient() { + if (this.scalarClient) return Promise.resolve(this.scalarClient); + if (ScalarAuthClient.isPossible()) { + this.scalarClient = new ScalarAuthClient(); + return this.scalarClient.connect().then(() => { + this.forceUpdate(); + return this.scalarClient; + }).catch((e) => { + this._imError(_td("Failed to connect to integrations server"), e); + }); + } else { + this._imError(_td("No integrations server is configured to manage stickers with")); + } + } + + async _removeStickerpickerWidgets() { + const scalarClient = await this._acquireScalarClient(); console.warn('Removing Stickerpicker widgets'); if (this.state.widgetId) { - this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => { - console.warn('Assets disabled'); - }).catch((err) => { - console.error('Failed to disable assets'); - }); + if (scalarClient) { + scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => { + console.warn('Assets disabled'); + }).catch((err) => { + console.error('Failed to disable assets'); + }); + } else { + console.error("Cannot disable assets: no scalar client"); + } } else { console.warn('No widget ID specified, not disabling assets'); } @@ -87,19 +109,7 @@ export default class Stickerpicker extends React.Component { // Close the sticker picker when the window resizes window.addEventListener('resize', this._onResize); - this.scalarClient = null; - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().then(() => { - this.forceUpdate(); - }).catch((e) => { - this._imError("Failed to connect to integrations server", e); - }); - } - - if (!this.state.imError) { - this.dispatcherRef = dis.register(this._onWidgetAction); - } + this.dispatcherRef = dis.register(this._onWidgetAction); // Track updates to widget state in account data MatrixClientPeg.get().on('accountData', this._updateWidget); @@ -126,7 +136,7 @@ export default class Stickerpicker extends React.Component { console.error(errorMsg, e); this.setState({ showStickers: false, - imError: errorMsg, + imError: _t(errorMsg), }); } @@ -339,22 +349,13 @@ export default class Stickerpicker extends React.Component { */ _launchManageIntegrations() { const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this.scalarClient.connect().done(() => { - const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom( - this.props.room, - 'type_' + widgetType, - this.state.widgetId, - ) : - null; - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, "mx_IntegrationsManager"); - this.setState({showStickers: false}); - }, (err) => { - this.setState({imError: err}); - console.error('Error ensuring a valid scalar_token exists', err); - }); + + // The integrations manager will handle scalar auth for us. + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: `type_${widgetType}`, + integrationId: this.state.widgetId, + }, "mx_IntegrationsManager"); } render() { diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index a517771f1d..754693b73e 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,50 +15,124 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import dis from '../../../dispatcher'; +import ScalarAuthClient from '../../../ScalarAuthClient'; -const React = require('react'); -const sdk = require('../../../index'); -const MatrixClientPeg = require('../../../MatrixClientPeg'); -const dis = require('../../../dispatcher'); +export default class IntegrationsManager extends React.Component { + static propTypes = { + // the room object where the integrations manager should be opened in + room: PropTypes.object.isRequired, -module.exports = React.createClass({ - displayName: 'IntegrationsManager', + // the screen name to open + screen: PropTypes.string, - propTypes: { - src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded - onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed - }, + // the integration ID to open + integrationId: PropTypes.string, - // XXX: keyboard shortcuts for managing dialogs should be done by the modal - // dialog base class somehow, surely... - componentDidMount: function() { + // callback when the manager is dismissed + onFinished: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + loading: true, + configured: ScalarAuthClient.isPossible(), + connected: false, // true if a `src` is set and able to be connected to + src: null, // string for where to connect to + }; + } + + componentWillMount() { + if (!this.state.configured) return; + + const scalarClient = new ScalarAuthClient(); + scalarClient.connect().then(() => { + const hasCredentials = scalarClient.hasCredentials(); + if (!hasCredentials) { + this.setState({ + connected: false, + loading: false, + }); + } else { + const src = scalarClient.getScalarInterfaceUrlForRoom( + this.props.room, + this.props.screen, + this.props.integrationId, + ); + this.setState({ + loading: false, + connected: true, + src: src, + }); + } + }).catch(err => { + console.error(err); + this.setState({ + loading: false, + connected: false, + }); + }); + } + + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onKeyDown); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); - }, + } - onKeyDown: function(ev) { - if (ev.keyCode == 27) { // escape + onKeyDown = (ev) => { + if (ev.keyCode === 27) { // escape ev.stopPropagation(); ev.preventDefault(); this.props.onFinished(); } - }, + }; - onAction: function(payload) { + onAction = (payload) => { if (payload.action === 'close_scalar') { this.props.onFinished(); } - }, + }; - render: function() { - return ( - - ); - }, -}); + render() { + if (!this.state.configured) { + return ( +
    +

    {_t("No integrations server configured")}

    +

    {_t("This Riot instance does not have an integrations server configured.")}

    +
    + ); + } + + if (this.state.loading) { + const Spinner = sdk.getComponent("elements.Spinner"); + return ( +
    +

    {_t("Connecting to integrations server...")}

    + +
    + ); + } + + if (!this.state.connected) { + return ( +
    +

    {_t("Cannot connect to integrations server")}

    +

    {_t("The integrations server is offline or it cannot reach your homeserver.")}

    +
    + ); + } + + return ; + } +} diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 84d70a48d4..eb85fe4e44 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -29,48 +29,64 @@ export default class VoiceUserSettingsTab extends React.Component { super(); this.state = { - mediaDevices: null, + mediaDevices: false, activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, }; } - componentWillMount(): void { - this._refreshMediaDevices(); + async componentDidMount() { + const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices(); + if (canSeeDeviceLabels) { + this._refreshMediaDevices(); + } } _refreshMediaDevices = async (stream) => { - if (stream) { - // kill stream so that we don't leave it lingering around with webcam enabled etc - // as here we called gUM to ask user for permission to their device names only - stream.getTracks().forEach((track) => track.stop()); - } - this.setState({ mediaDevices: await CallMediaHandler.getDevices(), activeAudioOutput: CallMediaHandler.getAudioOutput(), activeAudioInput: CallMediaHandler.getAudioInput(), activeVideoInput: CallMediaHandler.getVideoInput(), }); + if (stream) { + // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) + // so that we don't leave it lingering around with webcam enabled etc + // as here we called gUM to ask user for permission to their device names only + stream.getTracks().forEach((track) => track.stop()); + } }; - _requestMediaPermissions = () => { - const getUserMedia = ( - window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia - ); - if (getUserMedia) { - return getUserMedia.apply(window.navigator, [ - { video: true, audio: true }, - this._refreshMediaDevices, - function() { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { - title: _t('No media permissions'), - description: _t('You may need to manually permit Riot to access your microphone/webcam'), - }); - }, - ]); + _requestMediaPermissions = async () => { + let constraints; + let stream; + let error; + try { + constraints = {video: true, audio: true}; + stream = await navigator.mediaDevices.getUserMedia(constraints); + } catch (err) { + // user likely doesn't have a webcam, + // we should still allow to select a microphone + if (err.name === "NotFoundError") { + constraints = { audio: true }; + try { + stream = await navigator.mediaDevices.getUserMedia(constraints); + } catch (err) { + error = err; + } + } else { + error = err; + } + } + if (error) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { + title: _t('No media permissions'), + description: _t('You may need to manually permit Riot to access your microphone/webcam'), + }); + } else { + this._refreshMediaDevices(stream); } }; @@ -100,7 +116,9 @@ export default class VoiceUserSettingsTab extends React.Component { }; _renderDeviceOptions(devices, category) { - return devices.map((d) => ); + return devices.map((d) => { + return (); + }); } render() { diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js index b21153f2cc..baace2ca1e 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.js @@ -31,7 +31,7 @@ export default class VerificationCancelled extends React.Component { "The other party cancelled the verification.", )}

    diff --git a/src/createRoom.js b/src/createRoom.js index 39b634a0ef..120043247d 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -30,12 +30,15 @@ import {getAddressType} from "./UserAddress"; * @param {object=} opts parameters for creating the room * @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them * @param {object=} opts.createOpts set of options to pass to createRoom call. + * @param {bool=} opts.spinner True to show a modal spinner while the room is created. + * Default: True * * @returns {Promise} which resolves to the room id, or null if the * action was aborted or failed. */ function createRoom(opts) { opts = opts || {}; + if (opts.spinner === undefined) opts.spinner = true; const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const Loader = sdk.getComponent("elements.Spinner"); @@ -87,11 +90,12 @@ function createRoom(opts) { }, ]; - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + let modal; + if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); let roomId; return client.createRoom(createOpts).finally(function() { - modal.close(); + if (modal) modal.close(); }).then(function(res) { roomId = res.room_id; if (opts.dmUserId) { diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index 6cb5974729..2aedf8d7f5 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -15,22 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {UserPillPart, RoomPillPart, PlainPart} from "./parts"; - export default class AutocompleteWrapperModel { - constructor(updateCallback, getAutocompleterComponent, updateQuery, room, client) { + constructor(updateCallback, getAutocompleterComponent, updateQuery, partCreator) { this._updateCallback = updateCallback; this._getAutocompleterComponent = getAutocompleterComponent; this._updateQuery = updateQuery; + this._partCreator = partCreator; this._query = null; - this._room = room; - this._client = client; } onEscape(e) { this._getAutocompleterComponent().onEscape(e); this._updateCallback({ - replacePart: new PlainPart(this._queryPart.text), + replacePart: this._partCreator.plain(this._queryPart.text), caretOffset: this._queryOffset, close: true, }); @@ -93,21 +90,22 @@ export default class AutocompleteWrapperModel { } _partForCompletion(completion) { - const firstChr = completion.completionId && completion.completionId[0]; + const {completionId} = completion; + const text = completion.completion; + const firstChr = completionId && completionId[0]; switch (firstChr) { case "@": { - const displayName = completion.completion; - const userId = completion.completionId; - const member = this._room.getMember(userId); - return new UserPillPart(userId, displayName, member); - } - case "#": { - const displayAlias = completion.completionId; - return new RoomPillPart(displayAlias, this._client); + if (completionId === "@room") { + return this._partCreator.atRoomPill(completionId); + } else { + return this._partCreator.userPill(text, completionId); + } } + case "#": + return this._partCreator.roomPill(completionId); // also used for emoji completion default: - return new PlainPart(completion.completion); + return this._partCreator.plain(text); } } } diff --git a/src/editor/caret.js b/src/editor/caret.js index f93e9604d5..c56022d8c6 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -15,50 +15,104 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render"; + export function setCaretPosition(editor, model, caretPosition) { const sel = document.getSelection(); sel.removeAllRanges(); const range = document.createRange(); + const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, caretPosition); + const lineNode = editor.childNodes[lineIndex]; + + let focusNode; + // empty line with just a
    + if (nodeIndex === -1) { + focusNode = lineNode; + } else { + focusNode = lineNode.childNodes[nodeIndex]; + // make sure we have a text node + if (focusNode.nodeType === Node.ELEMENT_NODE && focusNode.firstChild) { + focusNode = focusNode.firstChild; + } + } + range.setStart(focusNode, offset); + range.collapse(true); + sel.addRange(range); +} + +function getLineAndNodePosition(model, caretPosition) { const {parts} = model; - const {index} = caretPosition; + const partIndex = caretPosition.index; + const lineResult = findNodeInLineForPart(parts, partIndex); + const {lineIndex} = lineResult; + let {nodeIndex} = lineResult; let {offset} = caretPosition; + // we're at an empty line between a newline part + // and another newline part or end/start of parts. + // set offset to 0 so it gets set to the
    inside the line container + if (nodeIndex === -1) { + offset = 0; + } else { + // move caret out of uneditable part (into caret node, or empty line br) if needed + ({nodeIndex, offset} = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset)); + } + return {lineIndex, nodeIndex, offset}; +} + +function findNodeInLineForPart(parts, partIndex) { let lineIndex = 0; let nodeIndex = -1; - for (let i = 0; i <= index; ++i) { + + let prevPart = null; + // go through to parts up till (and including) the index + // to find newline parts + for (let i = 0; i <= partIndex; ++i) { const part = parts[i]; - if (part && part.type === "newline") { - if (i < index) { - lineIndex += 1; - nodeIndex = -1; - } else { - // if index points at a newline part, - // put the caret at the end of the previous part - // so it stays on the same line - const prevPart = parts[i - 1]; - offset = prevPart ? prevPart.text.length : 0; + if (part.type === "newline") { + lineIndex += 1; + nodeIndex = -1; + prevPart = null; + } else { + nodeIndex += 1; + if (needsCaretNodeBefore(part, prevPart)) { + nodeIndex += 1; + } + // only jump over caret node if we're not at our destination node already, + // as we'll assume in moveOutOfUneditablePart that nodeIndex + // refers to the node corresponding to the part, + // and not an adjacent caret node + if (i < partIndex) { + const nextPart = parts[i + 1]; + const isLastOfLine = !nextPart || nextPart.type === "newline"; + if (needsCaretNodeAfter(part, isLastOfLine)) { + nodeIndex += 1; + } + } + prevPart = part; + } + } + + return {lineIndex, nodeIndex}; +} + +function moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset) { + // move caret before or after uneditable part + const part = parts[partIndex]; + if (part && !part.canEdit) { + if (offset === 0) { + nodeIndex -= 1; + const prevPart = parts[partIndex - 1]; + // if the previous node is a caret node, it's empty + // so the offset can stay at 0 + // only when it's not, we need to set the offset + // at the end of the node + if (!needsCaretNodeBefore(part, prevPart)) { + offset = prevPart.text.length; } } else { nodeIndex += 1; + offset = 0; } } - let focusNode; - const lineNode = editor.childNodes[lineIndex]; - if (lineNode) { - focusNode = lineNode.childNodes[nodeIndex]; - if (!focusNode) { - focusNode = lineNode; - } else if (focusNode.nodeType === Node.ELEMENT_NODE) { - focusNode = focusNode.childNodes[0]; - } - } - // node not found, set caret at end - if (!focusNode) { - range.selectNodeContents(editor); - range.collapse(false); - } else { - // make sure we have a text node - range.setStart(focusNode, offset); - range.collapse(true); - } - sel.addRange(range); + return {nodeIndex, offset}; } diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 48625cba5f..6507a8dc12 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -16,73 +16,92 @@ limitations under the License. */ import { MATRIXTO_URL_PATTERN } from '../linkify-matrix'; -import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts"; import { walkDOMDepthFirst } from "./dom"; const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); -function parseLink(a, room, client) { +function parseAtRoomMentions(text, partCreator) { + const ATROOM = "@room"; + const parts = []; + text.split(ATROOM).forEach((textPart, i, arr) => { + if (textPart.length) { + parts.push(partCreator.plain(textPart)); + } + // it's safe to never append @room after the last textPart + // as split will report an empty string at the end if + // `text` ended in @room. + const isLast = i === arr.length - 1; + if (!isLast) { + parts.push(partCreator.atRoomPill(ATROOM)); + } + }); + return parts; +} + +function parseLink(a, partCreator) { const {href} = a; const pillMatch = REGEX_MATRIXTO.exec(href) || []; const resourceId = pillMatch[1]; // The room/user ID const prefix = pillMatch[2]; // The first character of prefix switch (prefix) { case "@": - return new UserPillPart( - resourceId, - a.textContent, - room.getMember(resourceId), - ); + return partCreator.userPill(a.textContent, resourceId); case "#": - return new RoomPillPart(resourceId, client); + return partCreator.roomPill(resourceId); default: { if (href === a.textContent) { - return new PlainPart(a.textContent); + return partCreator.plain(a.textContent); } else { - return new PlainPart(`[${a.textContent}](${href})`); + return partCreator.plain(`[${a.textContent}](${href})`); } } } } -function parseCodeBlock(n) { +function parseCodeBlock(n, partCreator) { const parts = []; const preLines = ("```\n" + n.textContent + "```").split("\n"); preLines.forEach((l, i) => { - parts.push(new PlainPart(l)); + parts.push(partCreator.plain(l)); if (i < preLines.length - 1) { - parts.push(new NewlinePart("\n")); + parts.push(partCreator.newline()); } }); return parts; } -function parseElement(n, room, client) { +function parseElement(n, partCreator, state) { switch (n.nodeName) { case "A": - return parseLink(n, room, client); + return parseLink(n, partCreator); case "BR": - return new NewlinePart("\n"); + return partCreator.newline(); case "EM": - return new PlainPart(`*${n.textContent}*`); + return partCreator.plain(`*${n.textContent}*`); case "STRONG": - return new PlainPart(`**${n.textContent}**`); + return partCreator.plain(`**${n.textContent}**`); case "PRE": - return parseCodeBlock(n); + return parseCodeBlock(n, partCreator); case "CODE": - return new PlainPart(`\`${n.textContent}\``); + return partCreator.plain(`\`${n.textContent}\``); case "DEL": - return new PlainPart(`${n.textContent}`); - case "LI": + return partCreator.plain(`${n.textContent}`); + case "LI": { + const indent = " ".repeat(state.listDepth - 1); if (n.parentElement.nodeName === "OL") { - return new PlainPart(` 1. `); + return partCreator.plain(`${indent}1. `); } else { - return new PlainPart(` - `); + return partCreator.plain(`${indent}- `); } + } + case "OL": + case "UL": + state.listDepth = (state.listDepth || 0) + 1; + // es-lint-disable-next-line no-fallthrough default: // don't textify block nodes we'll decend into if (!checkDecendInto(n)) { - return new PlainPart(n.textContent); + return partCreator.plain(n.textContent); } } } @@ -125,22 +144,22 @@ function checkIgnored(n) { return true; } -function prefixQuoteLines(isFirstNode, parts) { +function prefixQuoteLines(isFirstNode, parts, partCreator) { const PREFIX = "> "; // a newline (to append a > to) wouldn't be added to parts for the first line // if there was no content before the BLOCKQUOTE, so handle that if (isFirstNode) { - parts.splice(0, 0, new PlainPart(PREFIX)); + parts.splice(0, 0, partCreator.plain(PREFIX)); } for (let i = 0; i < parts.length; i += 1) { if (parts[i].type === "newline") { - parts.splice(i + 1, 0, new PlainPart(PREFIX)); + parts.splice(i + 1, 0, partCreator.plain(PREFIX)); i += 1; } } } -function parseHtmlMessage(html, room, client) { +function parseHtmlMessage(html, partCreator) { // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine @@ -148,6 +167,7 @@ function parseHtmlMessage(html, room, client) { const parts = []; let lastNode; let inQuote = false; + const state = {}; function onNodeEnter(n) { if (checkIgnored(n)) { @@ -159,13 +179,13 @@ function parseHtmlMessage(html, room, client) { const newParts = []; if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) { - newParts.push(new NewlinePart("\n")); + newParts.push(partCreator.newline()); } if (n.nodeType === Node.TEXT_NODE) { - newParts.push(new PlainPart(n.nodeValue)); + newParts.push(...parseAtRoomMentions(n.nodeValue, partCreator)); } else if (n.nodeType === Node.ELEMENT_NODE) { - const parseResult = parseElement(n, room, client); + const parseResult = parseElement(n, partCreator, state); if (parseResult) { if (Array.isArray(parseResult)) { newParts.push(...parseResult); @@ -177,14 +197,14 @@ function parseHtmlMessage(html, room, client) { if (newParts.length && inQuote) { const isFirstPart = parts.length === 0; - prefixQuoteLines(isFirstPart, newParts); + prefixQuoteLines(isFirstPart, newParts, partCreator); } parts.push(...newParts); // extra newline after quote, only if there something behind it... if (lastNode && lastNode.nodeName === "BLOCKQUOTE") { - parts.push(new NewlinePart("\n")); + parts.push(partCreator.newline()); } lastNode = null; return checkDecendInto(n); @@ -194,8 +214,14 @@ function parseHtmlMessage(html, room, client) { if (checkIgnored(n)) { return; } - if (n.nodeName === "BLOCKQUOTE") { - inQuote = false; + switch (n.nodeName) { + case "BLOCKQUOTE": + inQuote = false; + break; + case "OL": + case "UL": + state.listDepth -= 1; + break; } lastNode = n; } @@ -205,23 +231,25 @@ function parseHtmlMessage(html, room, client) { return parts; } -export function parseEvent(event, room, client) { +export function parseEvent(event, partCreator) { const content = event.getContent(); + let parts; if (content.format === "org.matrix.custom.html") { - return parseHtmlMessage(content.formatted_body || "", room, client); + parts = parseHtmlMessage(content.formatted_body || "", partCreator); } else { const body = content.body || ""; const lines = body.split("\n"); - const parts = lines.reduce((parts, line, i) => { + parts = lines.reduce((parts, line, i) => { const isLast = i === lines.length - 1; - const text = new PlainPart(line); - const newLine = !isLast && new NewlinePart("\n"); - if (newLine) { - return parts.concat(text, newLine); - } else { - return parts.concat(text); + const newParts = parseAtRoomMentions(line, partCreator); + if (!isLast) { + newParts.push(partCreator.newline()); } + return parts.concat(newParts); }, []); - return parts; } + if (content.msgtype === "m.emote") { + parts.unshift(partCreator.plain("/me ")); + } + return parts; } diff --git a/src/editor/dom.js b/src/editor/dom.js index 3ef1df24c3..1b683c2c5e 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {CARET_NODE_CHAR, isCaretNode} from "./render"; + export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { let node = rootNode.firstChild; while (node && node !== rootNode) { @@ -38,27 +40,54 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback } export function getCaretOffsetAndText(editor, sel) { - let {focusNode} = sel; - const {focusOffset} = sel; - let caretOffset = focusOffset; + let {focusNode, focusOffset} = sel; + // sometimes focusNode is an element, and then focusOffset means + // the index of a child element ... - 1 🤷 + if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) { + focusNode = focusNode.childNodes[focusOffset - 1]; + focusOffset = focusNode.textContent.length; + } + const {text, focusNodeOffset} = getTextAndFocusNodeOffset(editor, focusNode, focusOffset); + const caret = getCaret(focusNode, focusNodeOffset, focusOffset); + return {caret, text}; +} + +// gets the caret position details, ignoring and adjusting to +// the ZWS if you're typing in a caret node +function getCaret(focusNode, focusNodeOffset, focusOffset) { + let atNodeEnd = focusOffset === focusNode.textContent.length; + if (focusNode.nodeType === Node.TEXT_NODE && isCaretNode(focusNode.parentElement)) { + const zwsIdx = focusNode.nodeValue.indexOf(CARET_NODE_CHAR); + if (zwsIdx !== -1 && zwsIdx < focusOffset) { + focusOffset -= 1; + } + // if typing in a caret node, you're either typing before or after the ZWS. + // In both cases, you should be considered at node end because the ZWS is + // not included in the text here, and once the model is updated and rerendered, + // that caret node will be removed. + atNodeEnd = true; + } + return {offset: focusNodeOffset + focusOffset, atNodeEnd}; +} + +// gets the text of the editor as a string, +// and the offset in characters where the focusNode starts in that string +// all ZWS from caret nodes are filtered out +function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { + let focusNodeOffset = 0; let foundCaret = false; let text = ""; - if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) { - focusNode = focusNode.childNodes[focusOffset - 1]; - caretOffset = focusNode.textContent.length; - } - function enterNodeCallback(node) { - const nodeText = node.nodeType === Node.TEXT_NODE && node.nodeValue; if (!foundCaret) { if (node === focusNode) { foundCaret = true; } } + const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node); if (nodeText) { if (!foundCaret) { - caretOffset += nodeText.length; + focusNodeOffset += nodeText.length; } text += nodeText; } @@ -73,14 +102,30 @@ export function getCaretOffsetAndText(editor, sel) { if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { text += "\n"; if (!foundCaret) { - caretOffset += 1; + focusNodeOffset += 1; } } } walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback); - const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; - const caret = {atNodeEnd, offset: caretOffset}; - return {caret, text}; + return {text, focusNodeOffset}; +} + +// get text value of text node, ignoring ZWS if it's a caret node +function getTextNodeValue(node) { + const nodeText = node.nodeValue; + // filter out ZWS for caret nodes + if (isCaretNode(node.parentElement)) { + // typed in the caret node, so there is now something more in it than the ZWS + // so filter out the ZWS, and take the typed text into account + if (nodeText.length !== 1) { + return nodeText.replace(CARET_NODE_CHAR, ""); + } else { + // only contains ZWS, which is ignored, so return emtpy string + return ""; + } + } else { + return nodeText; + } } diff --git a/src/editor/model.js b/src/editor/model.js index 04a56ab65b..1080df67ba 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -27,6 +27,10 @@ export default class EditorModel { this._updateCallback = updateCallback; } + clone() { + return new EditorModel(this._parts, this._partCreator, this._updateCallback); + } + _insertPart(index, part) { this._parts.splice(index, 0, part); if (this._activePartIdx >= index) { @@ -91,7 +95,7 @@ export default class EditorModel { const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; if (diff.removed) { - removedOffsetDecrease = this._removeText(position, diff.removed.length); + removedOffsetDecrease = this.removeText(position, diff.removed.length); } let addedLen = 0; if (diff.added) { @@ -99,8 +103,7 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - let newPosition = this.positionForOffset(caretOffset, true); - newPosition = newPosition.skipUneditableParts(this._parts); + const newPosition = this.positionForOffset(caretOffset, true); this._setActivePart(newPosition); this._updateCallback(newPosition); } @@ -136,10 +139,9 @@ export default class EditorModel { let pos; if (replacePart) { this._replacePart(this._autoCompletePartIdx, replacePart); - let index = this._autoCompletePartIdx; + const index = this._autoCompletePartIdx; if (caretOffset === undefined) { - caretOffset = 0; - index += 1; + caretOffset = replacePart.text.length; } pos = new DocumentPosition(index, caretOffset); } @@ -154,11 +156,11 @@ export default class EditorModel { } _mergeAdjacentParts(docPos) { - let prevPart = this._parts[0]; - for (let i = 1; i < this._parts.length; ++i) { + let prevPart; + for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; const isEmpty = !part.text.length; - const isMerged = !isEmpty && prevPart.merge(part); + const isMerged = !isEmpty && prevPart && prevPart.merge(part); if (isEmpty || isMerged) { // remove empty or merged part part = prevPart; @@ -177,7 +179,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - _removeText(pos, len) { + removeText(pos, len) { let {index, offset} = pos; let removedOffsetDecrease = 0; while (len > 0) { @@ -279,13 +281,4 @@ class DocumentPosition { get offset() { return this._offset; } - - skipUneditableParts(parts) { - const part = parts[this.index]; - if (part && !part.canEdit) { - return new DocumentPosition(this.index + 1, 0); - } else { - return this; - } - } } diff --git a/src/editor/parts.js b/src/editor/parts.js index a122c7ab7a..dc2c1e69a2 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -107,7 +107,7 @@ class BasePart { } } -export class PlainPart extends BasePart { +class PlainPart extends BasePart { acceptsInsertion(chr) { return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n"; } @@ -199,7 +199,7 @@ class PillPart extends BasePart { } } -export class NewlinePart extends BasePart { +class NewlinePart extends BasePart { acceptsInsertion(chr, i) { return (this.text.length + i) === 0 && chr === "\n"; } @@ -235,20 +235,10 @@ export class NewlinePart extends BasePart { } } -export class RoomPillPart extends PillPart { - constructor(displayAlias, client) { +class RoomPillPart extends PillPart { + constructor(displayAlias, room) { super(displayAlias, displayAlias); - this._room = this._findRoomByAlias(displayAlias, client); - } - - _findRoomByAlias(alias, client) { - if (alias[0] === '#') { - return client.getRooms().find((r) => { - return r.getAliases().includes(alias); - }); - } else { - return client.getRoom(alias); - } + this._room = room; } setAvatar(node) { @@ -270,7 +260,13 @@ export class RoomPillPart extends PillPart { } } -export class UserPillPart extends PillPart { +class AtRoomPillPart extends RoomPillPart { + get type() { + return "at-room-pill"; + } +} + +class UserPillPart extends PillPart { constructor(userId, displayName, member) { super(userId, displayName); this._member = member; @@ -311,7 +307,7 @@ export class UserPillPart extends PillPart { } -export class PillCandidatePart extends PlainPart { +class PillCandidatePart extends PlainPart { constructor(text, autoCompleteCreator) { super(text); this._autoCompleteCreator = autoCompleteCreator; @@ -351,8 +347,7 @@ export class PartCreator { updateCallback, getAutocompleterComponent, updateQuery, - room, - client, + this, ); }; } @@ -362,7 +357,7 @@ export class PartCreator { case "#": case "@": case ":": - return new PillCandidatePart("", this._autoCompleteCreator); + return this.pillCandidate(""); case "\n": return new NewlinePart(); default: @@ -371,24 +366,57 @@ export class PartCreator { } createDefaultPart(text) { - return new PlainPart(text); + return this.plain(text); } deserializePart(part) { switch (part.type) { case "plain": - return new PlainPart(part.text); + return this.plain(part.text); case "newline": - return new NewlinePart(part.text); + return this.newline(); + case "at-room-pill": + return this.atRoomPill(part.text); case "pill-candidate": - return new PillCandidatePart(part.text, this._autoCompleteCreator); + return this.pillCandidate(part.text); case "room-pill": - return new RoomPillPart(part.text, this._client); - case "user-pill": { - const member = this._room.getMember(part.userId); - return new UserPillPart(part.userId, part.text, member); - } + return this.roomPill(part.text); + case "user-pill": + return this.userPill(part.text, part.userId); } } + + plain(text) { + return new PlainPart(text); + } + + newline() { + return new NewlinePart("\n"); + } + + pillCandidate(text) { + return new PillCandidatePart(text, this._autoCompleteCreator); + } + + roomPill(alias) { + let room; + if (alias[0] === '#') { + room = this._client.getRooms().find((r) => { + return r.getAliases().includes(alias); + }); + } else { + room = this._client.getRoom(alias); + } + return new RoomPillPart(alias, room); + } + + atRoomPill(text) { + return new AtRoomPillPart(text, this._room); + } + + userPill(displayName, userId) { + const member = this._room.getMember(userId); + return new UserPillPart(userId, displayName, member); + } } diff --git a/src/editor/render.js b/src/editor/render.js index 58ef0eaee1..9d42bbe947 100644 --- a/src/editor/render.js +++ b/src/editor/render.js @@ -15,6 +15,137 @@ See the License for the specific language governing permissions and limitations under the License. */ +export function needsCaretNodeBefore(part, prevPart) { + const isFirst = !prevPart || prevPart.type === "newline"; + return !part.canEdit && (isFirst || !prevPart.canEdit); +} + +export function needsCaretNodeAfter(part, isLastOfLine) { + return !part.canEdit && isLastOfLine; +} + +function insertAfter(node, nodeToInsert) { + const next = node.nextSibling; + if (next) { + node.parentElement.insertBefore(nodeToInsert, next); + } else { + node.parentElement.appendChild(nodeToInsert); + } +} + +// Use a BOM marker for caret nodes. +// On a first test, they seem to be filtered out when copying text out of the editor, +// but this could be platform dependent. +// As a precautionary measure, I chose the character that slate also uses. +export const CARET_NODE_CHAR = "\ufeff"; +// a caret node is a node that allows the caret to be placed +// where otherwise it wouldn't be possible +// (e.g. next to a pill span without adjacent text node) +function createCaretNode() { + const span = document.createElement("span"); + span.className = "caretNode"; + span.appendChild(document.createTextNode(CARET_NODE_CHAR)); + return span; +} + +function updateCaretNode(node) { + // ensure the caret node contains only a zero-width space + if (node.textContent !== CARET_NODE_CHAR) { + node.textContent = CARET_NODE_CHAR; + } +} + +export function isCaretNode(node) { + return node && node.tagName === "SPAN" && node.className === "caretNode"; +} + +function removeNextSiblings(node) { + if (!node) { + return; + } + node = node.nextSibling; + while (node) { + const removeNode = node; + node = node.nextSibling; + removeNode.remove(); + } +} + +function removeChildren(parent) { + const firstChild = parent.firstChild; + if (firstChild) { + removeNextSiblings(firstChild); + firstChild.remove(); + } +} + +function reconcileLine(lineContainer, parts) { + let currentNode; + let prevPart; + const lastPart = parts[parts.length - 1]; + + for (const part of parts) { + const isFirst = !prevPart; + currentNode = isFirst ? lineContainer.firstChild : currentNode.nextSibling; + + if (needsCaretNodeBefore(part, prevPart)) { + if (isCaretNode(currentNode)) { + updateCaretNode(currentNode); + currentNode = currentNode.nextSibling; + } else { + lineContainer.insertBefore(createCaretNode(), currentNode); + } + } + // remove nodes until matching current part + while (currentNode && !part.canUpdateDOMNode(currentNode)) { + const nextNode = currentNode.nextSibling; + lineContainer.removeChild(currentNode); + currentNode = nextNode; + } + // update or insert node for current part + if (currentNode && part) { + part.updateDOMNode(currentNode); + } else if (part) { + currentNode = part.toDOMNode(); + // hooks up nextSibling for next iteration + lineContainer.appendChild(currentNode); + } + + if (needsCaretNodeAfter(part, part === lastPart)) { + if (isCaretNode(currentNode.nextSibling)) { + currentNode = currentNode.nextSibling; + updateCaretNode(currentNode); + } else { + const caretNode = createCaretNode(); + insertAfter(currentNode, caretNode); + currentNode = caretNode; + } + } + + prevPart = part; + } + + removeNextSiblings(currentNode); +} + +function reconcileEmptyLine(lineContainer) { + // empty div needs to have a BR in it to give it height + let foundBR = false; + let partNode = lineContainer.firstChild; + while (partNode) { + const nextNode = partNode.nextSibling; + if (!foundBR && partNode.tagName === "BR") { + foundBR = true; + } else { + partNode.remove(); + } + partNode = nextNode; + } + if (!foundBR) { + lineContainer.appendChild(document.createElement("br")); + } +} + export function renderModel(editor, model) { const lines = model.parts.reduce((lines, part) => { if (part.type === "newline") { @@ -25,8 +156,9 @@ export function renderModel(editor, model) { } return lines; }, [[]]); - // TODO: refactor this code, DRY it lines.forEach((parts, i) => { + // find first (and remove anything else) div without className + // (as browsers insert these in contenteditable) line container let lineContainer = editor.childNodes[i]; while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) { editor.removeChild(lineContainer); @@ -38,46 +170,14 @@ export function renderModel(editor, model) { } if (parts.length) { - parts.forEach((part, j) => { - let partNode = lineContainer.childNodes[j]; - while (partNode && !part.canUpdateDOMNode(partNode)) { - lineContainer.removeChild(partNode); - partNode = lineContainer.childNodes[j]; - } - if (partNode && part) { - part.updateDOMNode(partNode); - } else if (part) { - lineContainer.appendChild(part.toDOMNode()); - } - }); - - let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length); - while (surplusElementCount) { - lineContainer.removeChild(lineContainer.lastChild); - --surplusElementCount; - } + reconcileLine(lineContainer, parts); } else { - // empty div needs to have a BR in it to give it height - let foundBR = false; - let partNode = lineContainer.firstChild; - while (partNode) { - const nextNode = partNode.nextSibling; - if (!foundBR && partNode.tagName === "BR") { - foundBR = true; - } else { - lineContainer.removeChild(partNode); - } - partNode = nextNode; - } - if (!foundBR) { - lineContainer.appendChild(document.createElement("br")); - } - } - - let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length); - while (surplusElementCount) { - editor.removeChild(editor.lastChild); - --surplusElementCount; + reconcileEmptyLine(lineContainer); } }); + if (lines.length) { + removeNextSiblings(editor.children[lines.length]); + } else { + removeChildren(editor); + } } diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 73fbbe5d01..876130074c 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -24,6 +24,7 @@ export function mdSerialize(model) { return html + "\n"; case "plain": case "pill-candidate": + case "at-room-pill": return html + part.text; case "room-pill": case "user-pill": @@ -47,6 +48,7 @@ export function textSerialize(model) { return text + "\n"; case "plain": case "pill-candidate": + case "at-room-pill": return text + part.text; case "room-pill": case "user-pill": @@ -58,13 +60,11 @@ export function textSerialize(model) { export function requiresHtml(model) { return model.parts.some(part => { switch (part.type) { - case "newline": - case "plain": - case "pill-candidate": - return false; case "room-pill": case "user-pill": return true; + default: + return false; } }); } diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 83e12507ca..b48232b085 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -1971,5 +1971,32 @@ "Invalid base_url for m.homeserver": "Невалиден base_url в m.homeserver", "Homeserver URL does not appear to be a valid Matrix homeserver": "Homeserver адресът не изглежда да е валиден Matrix сървър", "Invalid base_url for m.identity_server": "Невалиден base_url в m.identity_server", - "Identity server URL does not appear to be a valid identity server": "Адресът на сървърът за самоличност не изглежда да е валиден сървър за самоличност" + "Identity server URL does not appear to be a valid identity server": "Адресът на сървърът за самоличност не изглежда да е валиден сървър за самоличност", + "Cannot reach homeserver": "Неуспешна връзка със сървъра", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Уверете се, че интернет връзката ви е стабилна, или се свържете с администратора на сървъра", + "Your Riot is misconfigured": "Riot не е конфигуриран правилно", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Попитайте Riot администратора да провери конфигурацията ви за неправилни или дублирани записи.", + "Cannot reach identity server": "Неуспешна връзка със сървъра за самоличност", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Може да се регистрирате, но някои функции няма да са достъпни докато сървъра за самоличност е офлайн. Ако продължавате да виждате това предупреждение, проверете конфигурацията или се свържете с администратора на сървъра.", + "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.": "Може да възстановите паролата си, но някои функции няма да са достъпни докато сървъра за самоличност е офлайн. Ако продължавате да виждате това предупреждение, проверете конфигурацията или се свържете с администратора на сървъра.", + "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.": "Може да влезете в профила си, но някои функции няма да са достъпни докато сървъра за самоличност е офлайн. Ако продължавате да виждате това предупреждение, проверете конфигурацията или се свържете с администратора на сървъра.", + "Unexpected error resolving identity server configuration": "Неочаквана грешка при откриване на конфигурацията на сървъра за самоличност", + "Use lowercase letters, numbers, dashes and underscores only": "Използвайте само малки букви, цифри, тирета и подчерта", + "Log in to your new account.": "Влезте в новия си профил.", + "You can now close this window or log in to your new account.": "Можете да затворите този прозорец или да влезете в новия си профил.", + "Registration Successful": "Успешна регистрация", + "No integrations server configured": "Не е конфигуриран сървър за интеграции", + "This Riot instance does not have an integrations server configured.": "Тази Riot инсталация няма конфигуриран сървър за интеграции.", + "Connecting to integrations server...": "Свързване към сървъра за интеграции...", + "Cannot connect to integrations server": "Неуспешна връзка към сървъра за интеграции", + "The integrations server is offline or it cannot reach your homeserver.": "Сървърът за интеграции не работи или не може да се свърже с вашия сървър.", + "Unnamed microphone": "Микрофон без име", + "Unnamed audio output": "Аудио изход без име", + "Unnamed camera": "Камера без име", + "Failed to connect to integrations server": "Неуспешно свързване със сървъра за интеграции", + "No integrations server is configured to manage stickers with": "Няма конфигуриран сървър за интеграции, с който да се управляват стикерите", + "Upload all": "Качи всички", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Новият ви профил (%(newAccountId)s) е регистриран, но вече сте влезли с друг профил (%(loggedInUserId)s).", + "Continue with previous account": "Продължи с предишния профил", + "Sign out of previous account": "Излез от предишния профил" } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index d4c0d8e799..44b991c0c9 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1905,5 +1905,9 @@ "Your Riot is misconfigured": "Riot je špatně nakonfigurován", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Zeptejte se svého administrátora, jestli by vám nezkontrolovat konfiguraci Riotu, asi obsahuje chyby nebo duplicity.", "Unexpected error resolving identity server configuration": "Chyba při hledání konfigurace serveru identity", - "Use lowercase letters, numbers, dashes and underscores only": "Používejte pouze malá písmena, čísla, pomlčky a podtržítka" + "Use lowercase letters, numbers, dashes and underscores only": "Používejte pouze malá písmena, čísla, pomlčky a podtržítka", + "Cannot reach identity server": "Nelze se připojit k serveru identity", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete se zaregistrovat, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte administrátora serveru.", + "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.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte administrátora serveru.", + "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.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte administrátora serveru." } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 38d42e2993..a3ad566d9e 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1808,5 +1808,9 @@ "Could not load user profile": "Konnte Nutzerprofil nicht laden", "Your Matrix account": "Dein Matrixkonto", "Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s", - "Show recent room avatars above the room list": "Zeige die letzten Avatare über der Raumliste an (neu laden um Änderungen zu übernehmen)" + "Show recent room avatars above the room list": "Zeige die letzten Avatare über der Raumliste an (neu laden um Änderungen zu übernehmen)", + "Email, name or Matrix ID": "E-Mail, Name oder Matrix-ID", + "Name or Matrix ID": "Name oder Matrix ID", + "Your Riot is misconfigured": "Dein Riot ist falsch konfiguriert", + "You cannot modify widgets in this room.": "Du kannst in diesem Raum keine Widgets verändern" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 53fd82f6f2..769316cd3b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -372,7 +372,7 @@ "Decline": "Decline", "Accept": "Accept", "The other party cancelled the verification.": "The other party cancelled the verification.", - "Cancel": "Cancel", + "OK": "OK", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", @@ -380,6 +380,7 @@ "Verify this user by confirming the following emoji appear on their screen.": "Verify this user by confirming the following emoji appear on their screen.", "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", "Unable to find a supported verification method.": "Unable to find a supported verification method.", + "Cancel": "Cancel", "For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", "Dog": "Dog", "Cat": "Cat", @@ -483,6 +484,11 @@ "Email Address": "Email Address", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", + "No integrations server configured": "No integrations server configured", + "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", + "Connecting to integrations server...": "Connecting to integrations server...", + "Cannot connect to integrations server": "Cannot connect to integrations server", + "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -513,7 +519,6 @@ "An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.", "Keywords": "Keywords", "Enter keywords separated by a comma:": "Enter keywords separated by a comma:", - "OK": "OK", "Failed to change settings": "Failed to change settings", "Can't update user notification settings": "Can't update user notification settings", "Failed to update keywords": "Failed to update keywords", @@ -827,6 +832,7 @@ "Join the conversation with an account": "Join the conversation with an account", "Sign Up": "Sign Up", "Sign In": "Sign In", + "Loading room preview": "Loading room preview", "You were kicked from %(roomName)s by %(memberName)s": "You were kicked from %(roomName)s by %(memberName)s", "Reason: %(reason)s": "Reason: %(reason)s", "Forget this room": "Forget this room", @@ -864,6 +870,8 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", + "Failed to connect to integrations server": "Failed to connect to integrations server", + "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -918,8 +926,6 @@ "Today": "Today", "Yesterday": "Yesterday", "Error decrypting audio": "Error decrypting audio", - "Agree or Disagree": "Agree or Disagree", - "Like or Dislike": "Like or Dislike", "Reply": "Reply", "Edit": "Edit", "Options": "Options", @@ -930,6 +936,13 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Error decrypting video": "Error decrypting video", + "Agree": "Agree", + "Disagree": "Disagree", + "Happy": "Happy", + "Party Popper": "Party Popper", + "Confused": "Confused", + "Eyes": "Eyes", + "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", @@ -940,7 +953,7 @@ "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", - "Edited at %(date)s": "Edited at %(date)s", + "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", "edited": "edited", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", @@ -1017,7 +1030,6 @@ "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", - "Integrations Error": "Integrations Error", "Manage Integrations": "Manage Integrations", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", @@ -1136,7 +1148,6 @@ "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)", "To continue, please enter your password:": "To continue, please enter your password:", - "password": "password", "Verify device": "Verify device", "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", "Verify by comparing a short text string.": "Verify by comparing a short text string.", @@ -1187,6 +1198,7 @@ "Manually export keys": "Manually export keys", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", + "Message edits": "Message edits", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "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.": "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.", "Report bugs & give feedback": "Report bugs & give feedback", @@ -1196,7 +1208,7 @@ "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", "Upgrade Room Version": "Upgrade Room Version", - "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:", "Create a new room with the same name, description and avatar": "Create a new room with the same name, description and avatar", "Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", @@ -1248,6 +1260,7 @@ "Unknown devices": "Unknown devices", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", + "Upload all": "Upload all", "Upload": "Upload", "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", @@ -1325,7 +1338,6 @@ "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.": "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.", "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.", "To continue, please enter your password.": "To continue, please enter your password.", - "Password:": "Password:", "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:", "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", @@ -1449,8 +1461,6 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "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.": "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.", - "You are logged in to another account": "You are logged in to another account", - "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.", "Logout": "Logout", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", @@ -1557,6 +1567,8 @@ "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", + "Continue with previous account": "Continue with previous account", "Log in to your new account.": "Log in to your new account.", "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 24d52f9331..1718b05cda 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -103,7 +103,7 @@ "Unknown (user, device) pair:": "Nekonata duopo (uzanto, aparato):", "Device already verified!": "Aparato jam kontroliĝis!", "WARNING: Device already verified, but keys do NOT MATCH!": "AVERTO: Aparato jam kontroliĝis, sed la ŝlosiloj NE KONGRUAS!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "AVERTO: KONTROLO DE ŜLOSILO MALSUKCESIS! Subskriba ŝlosilo por %(userId)s kaj aparato%(deviceId)s estas \"%(fprint)s\", kiu ne kongruas kun la donita ŝlosilo \"%(fingerprint)s\". Eble do via komuniko estas subaŭskultata!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "AVERTO: KONTROLO DE ŜLOSILO MALSUKCESIS! Subskriba ŝlosilo por %(userId)s kaj aparato%(deviceId)s estas « %(fprint)s », kiu ne kongruas kun la donita ŝlosilo « %(fingerprint)s ». Eble do via komuniko estas subaŭskultata!", "Verified key": "Kontrolita ŝlosilo", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "La donita subskriba ŝlosilo kongruas kun la ŝlosilo ricevita de %(userId)s por ĝia aparato %(deviceId)s. Aparato markita kiel kontrolita.", "Unrecognised command:": "Nerekonita komando:", @@ -126,7 +126,7 @@ "%(senderName)s unbanned %(targetName)s.": "%(senderName)s malbaris uzanton %(targetName)s.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s forpelis uzanton %(targetName)s.", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s nuligis inviton por %(targetName)s.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ŝanĝis la temon al \"%(topic)s\".", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ŝanĝis la temon al « %(topic)s ».", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s forigis nomon de la ĉambro.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ŝanĝis nomon de la ĉambro al %(roomName)s.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sendis bildon.", @@ -683,7 +683,7 @@ "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Vi nun malpermesas legadon de ĉifritaj mesaĝoj al nekontrolitaj aparatoj; por sendi mesaĝojn al tiuj, vi devas ilin kontroli.", "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Ni rekomendas al vi bone kontroli ĉiun aparaton por certigi, ke ĝi apartenas al la verŝajna posedanto, sed vi povas resendi la mesaĝon sen kontrolo, laŭprefere.", "Room contains unknown devices": "Ĉambro enhavas nekonatajn aparatojn", - "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" enhavas aparatojn, kiujn vi neniam vidis antaŭe.", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "« %(RoomName)s » enhavas aparatojn, kiujn vi neniam vidis antaŭe.", "Unknown devices": "Nekonataj aparatoj", "Private Chat": "Privata babilo", "Public Chat": "Publika babilo", @@ -697,7 +697,7 @@ "You must register to use this functionality": "Vi devas registriĝî por uzi tiun ĉi funkcion", "You must join the room to see its files": "Vi devas aliĝi al la ĉambro por vidi tie dosierojn", "There are no visible files in this room": "En ĉi tiu ĉambro estas neniaj videblaj dosieroj", - "

    HTML for your community's page

    \n

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

    \n

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

    \n": "

    HTML por la paĝo de via komunumo

    \n

    \n Uzu la longan priskribon por enkonduki novajn komunumanojn, aŭ disdoni iujn\n gravajn ligilojn\n

    \n

    \n Vi povas eĉ uzi etikedojn 'img'\n

    \n", + "

    HTML for your community's page

    \n

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

    \n

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

    \n": "

    HTML por la paĝo de via komunumo

    \n

    \n Uzu la longan priskribon por enkonduki novajn komunumanojn, aŭ disdoni iujn\n gravajn ligilojn\n

    \n

    \n Vi povas eĉ uzi etikedojn « img »\n

    \n", "Add rooms to the community summary": "Aldoni ĉambrojn al la komunuma superrigardo", "Which rooms would you like to add to this summary?": "Kiujn ĉambrojn vi volas aldoni al ĉi tiu superrigardo?", "Add to summary": "Aldoni al superrigardo", @@ -1146,9 +1146,9 @@ "Reversed words aren't much harder to guess": "Renversitaj vortoj ne estas multe pli malfacile konjekteblaj", "Predictable substitutions like '@' instead of 'a' don't help very much": "Facile diveneblaj anstataŭigoj, kiel '@' anstataŭ 'a', ne helpas multe", "Add another word or two. Uncommon words are better.": "Aldonu alian vorton aŭ du. Maloftaj vortoj pli bonas.", - "Repeats like \"aaa\" are easy to guess": "Ripetoj kiel \"aaa\" estas facile diveneblaj", - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Ripetoj kiel \"abcabcabc\" estas apenaŭ pli bonaj ol nur \"abc\"", - "Sequences like abc or 6543 are easy to guess": "Sinsekvoj kiel \"abc\" aŭ \"6543\" estas facile diveneblaj", + "Repeats like \"aaa\" are easy to guess": "Ripetoj kiel « aaa » estas facile diveneblaj", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Ripetoj kiel « abcabcabc » estas apenaŭ pli bonaj ol nur « abc »", + "Sequences like abc or 6543 are easy to guess": "Sinsekvoj kiel « abc » aŭ « 6543 » estas facile diveneblaj", "Recent years are easy to guess": "Freŝdataj jaroj estas facile diveneblaj", "Dates are often easy to guess": "Datoj estas ofte facile diveneblaj", "This is a top-10 common password": "Ĉi tiu pasvorto estas inter la 10 plej oftaj", @@ -1396,7 +1396,7 @@ "A conference call could not be started because the integrations server is not available": "Grupa voko ne povis komenciĝi, ĉar la kuniga servilo estas neatingebla", "Replying With Files": "Respondado kun dosieroj", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nun ne eblas respondi kun dosiero. Ĉu vi volas alŝuti la dosieron sen respondo?", - "The file '%(fileName)s' failed to upload.": "Malsukcesis alŝuti dosieron «%(fileName)s».", + "The file '%(fileName)s' failed to upload.": "Malsukcesis alŝuti dosieron « %(fileName)s ».", "The server does not support the room version specified.": "La servilo ne subtenas la donitan ĉambran version.", "Name or Matrix ID": "Nomo aŭ Matrix-identigilo", "Email, name or Matrix ID": "Retpoŝtadreso, nomo, aŭ Matrix-identigilo", @@ -1508,7 +1508,7 @@ "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ŝanĝoj al viaj komunumaj nomo kaj profilbildo eble ne montriĝos al aliaj uzantoj ĝis 30 minutoj.", "Who can join this community?": "Kiu povas aliĝi al tiu ĉi komunumo?", "This room is not public. You will not be able to rejoin without an invite.": "Ĉi tiu ĉambro ne estas publika. Vi ne povos realiĝi sen invito.", - "Can't leave Server Notices room": "Ne eblas eliri el ĉambro « Server Notices »", + "Can't leave Server Notices room": "Ne eblas eliri el ĉambro «  Server Notices  »", "Revoke invite": "Nuligi inviton", "Invited by %(sender)s": "Invitita de %(sender)s", "Error updating main address": "Eraro dum ĝisdatigo de la ĉefa adreso", @@ -1547,5 +1547,55 @@ "Your Riot is misconfigured": "Via kliento Riot estas misagordita", "All devices for this user are trusted": "Ĉiuj aparatoj de tiu ĉi uzanto estas fidataj", "All devices in this encrypted room are trusted": "Ĉiuj aparatoj en ĉi tiu ĉifrita ĉambro estas fidataj", - "Your key share request has been sent - please check your other devices for key share requests.": "Via peto por havigo de ŝlosilo sendiĝis – bonvolu kontroli viajn aliajn aparatojn pro petoj." + "Your key share request has been sent - please check your other devices for key share requests.": "Via peto por havigo de ŝlosilo sendiĝis – bonvolu kontroli viajn aliajn aparatojn pro petoj.", + "At this time it is not possible to reply with an emote.": "Ankoraŭ ne eblas respondi per mieno.", + "Joining room …": "Aliĝanta al ĉambro …", + "Loading …": "Enleganta …", + "Rejecting invite …": "Rifuzanta inviton …", + "Join the conversation with an account": "Aliĝu al la interparolo per konto", + "Sign Up": "Registriĝi", + "Sign In": "Saluti", + "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s forpelis vin de %(roomName)s", + "Reason: %(reason)s": "Kialo: %(reason)s", + "Forget this room": "Forgesi ĉi tiun ĉambron", + "Re-join": "Re-aliĝi", + "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vin forbaris de %(roomName)s", + "Something went wrong with your invite to %(roomName)s": "Io misokazis al via invito al %(roomName)s", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "Okazis eraro %(errcode)s dum kontrolado de via invito. Eble transdonu tiun informon al ĉambrestro.", + "You can only join it with a working invite.": "Vi povas aliĝi nur kun funkcianta invito.", + "You can still join it because this is a public room.": "Tamen vi povas aliĝi, ĉar ĉi tiu ĉambro estas publika.", + "Join the discussion": "Aliĝi al la diskuto", + "Try to join anyway": "Tamen provi aliĝi", + "This invite to %(roomName)s wasn't sent to your account": "Ĉi tiu invito al %(roomName)s ne sendiĝis al via konto", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Salutu per alia konto, petu inviton, aŭ aldonu la retpoŝtadreson %(email)s al ĉi tiu konto.", + "Do you want to chat with %(user)s?": "Ĉu vi volas babili kun %(user)s?", + "Do you want to join %(roomName)s?": "Ĉu vi volas aliĝi al %(roomName)s?", + " invited you": " vin invitis", + "You're previewing %(roomName)s. Want to join it?": "Vi antaŭrigardas ĉambron %(roomName)s. Ĉu vi volas aliĝi?", + "%(roomName)s can't be previewed. Do you want to join it?": "Vi ne povas antaŭrigardi ĉambron %(roomName)s. Ĉu vi al ĝi volas aliĝi?", + "This room doesn't exist. Are you sure you're at the right place?": "Ĉi tiu ĉambro ne ekzistas. Ĉu vi certe provas la ĝustan lokon?", + "Try again later, or ask a room admin to check if you have access.": "Reprovu poste, aŭ petu administranton kontroli, ĉu vi rajtas aliri.", + "%(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.": "Okazis eraro %(errcode)s dum aliro al la ĉambro. Se vi pensas, ke vi mise vidas la eraron, bonvolu raporti problemon.", + "Never lose encrypted messages": "Neniam perdu ĉifritajn mesaĝojn", + "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Mesaĝoj en ĉi tiu ĉambro estas tutvoje ĉifrataj. Nur vi kaj la ricevonto(j) havas la ŝlosilojn necesajn por ilin legi.", + "Securely back up your keys to avoid losing them. Learn more.": "Savkopiu sekure viajn ŝlosilojn por ilin ne perdi. Eksciu plion.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Gradaltigo de la ĉambro forigos la nunan ĉambron kaj kreos novan kun la sama nomo.", + "You don't currently have any stickerpacks enabled": "Vi havas neniujn ŝaltitajn glumarkarojn", + "Add some now": "Iujn aldoni", + "Stickerpack": "Glumarkaro", + "Hide Stickers": "Kaŝi glumarkojn", + "Show Stickers": "Montri glumarkojn", + "Failed to revoke invite": "Malsukcesis senvalidigi inviton", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Ne povis senvalidigi inviton. Aŭ la servilo nun trairas problemon, aŭ vi ne havas sufiĉajn permesojn.", + "Continue With Encryption Disabled": "Pluigi sen ĉifrado", + "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.": "Ĉi tio igos vian konton daŭre neuzebla. Vi ne povos saluti, kaj neniu povos reregistri la saman identigilon de uzanto. Ĝi foririgos vian konton de ĉiuj enataj ĉambroj, kaj forigos detalojn de via konto de la identeca servilo. Tiun agon ne eblas malfari.", + "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.": "Malaktivigo de via konto implicite ne forgesigas viajn mesaĝojn al ni. Se vi volas, ke ni ilin forgesu, bonvolu marki la suban markbutonon.", + "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.": "Videbleco de mesaĝoj en Matrix similas tiun de retpoŝto. Nia forgeso de viaj mesaĝoj signifas, ke ili haviĝos al neniu nova aŭ neregistrita uzanto, sed registritaj uzantoj, kiuj jam havas viajn mesaĝojn, ankoraŭ povos aliri siajn kopiaĵojn.", + "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)": "Bonvolu dum malaktivigo forgesi ĉiujn mesaĝojn, kiujn mi sendis. (Averto: tio vidigos al osaj uzantoj neplenajn interparolojn.)", + "Use Legacy Verification (for older clients)": "Uzi malnovecan kontrolon (por malnovaj klientoj)", + "Verify by comparing a short text string.": "Kontrolu per komparo de mallonga teksto.", + "Begin Verifying": "Komenci kontrolon", + "Waiting for partner to accept...": "Atendanta akcepton de kunulo…", + "Nothing appearing? Not all clients support interactive verification yet. .": "Ĉu neniu aperas? Ankoraŭ ne ĉiuj klientoj subtenas interagan kontrolon. .", + "Waiting for %(userId)s to confirm...": "Atendanta konfirmon de %(userId)s…" } diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 43e0d25d51..f0e3013090 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1878,7 +1878,7 @@ "Reason: %(reason)s": "Arrazoia: %(reason)s", "Forget this room": "Ahaztu gela hau", "Re-join": "Berriro elkartu", - "You were banned from %(roomName)s by %(memberName)s": "%(roomName)s gelan sartzea debekatu dizu %(memberName) erabiltzaileak", + "You were banned from %(roomName)s by %(memberName)s": "%(roomName)s gelan sartzea debekatu dizu %(memberName)s erabiltzaileak", "Something went wrong with your invite to %(roomName)s": "Arazo bat egon da zure %(roomName)s gelarako gonbidapenarekin", "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s jaso da zure gonbidapena balioztatzean. Saiatu informazio hau gelako administratzaileari ematen.", "You can only join it with a working invite.": "Elkartzeko baliozko gonbidapen bat behar duzu.", @@ -1942,5 +1942,26 @@ "Your profile": "Zure profila", "Your Matrix account on ": "Zure zerbitzariko Matrix kontua", "Homeserver URL does not appear to be a valid Matrix homeserver": "Hasiera-zerbitzariaren URL-a ez dirudi baliozko hasiera-zerbitzari batena", - "Identity server URL does not appear to be a valid identity server": "Identitate-zerbitzariaren URL-a ez dirudi baliozko identitate-zerbitzari batena" + "Identity server URL does not appear to be a valid identity server": "Identitate-zerbitzariaren URL-a ez dirudi baliozko identitate-zerbitzari batena", + "Cannot reach identity server": "Ezin izan da identitate-zerbitzaria atzitu", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Izena eman dezakezu, baina ezaugarri batzuk ez dira eskuragarri izango identitate-zerbitzaria berriro eskuragarri egon arte. Abisu hau ikusten jarraitzen baduzu, egiaztatu zure konfigurazioa edo kontaktatu zerbitzariaren administratzailea.", + "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.": "Zure pasahitza berrezarri dezakezu, baina ezaugarri batzuk ez dira eskuragarri izango identitate-zerbitzaria berriro eskuragarri egon arte. Abisu hau ikusten jarraitzen baduzu, egiaztatu zure konfigurazioa edo kontaktatu zerbitzariaren administratzailea.", + "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.": "Saioa hasi dezakezu, baina ezaugarri batzuk ez dira eskuragarri izango identitate-zerbitzaria berriro eskuragarri egon arte. Abisu hau ikusten jarraitzen baduzu, egiaztatu zure konfigurazioa edo kontaktatu zerbitzariaren administratzailea.", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Ezin izan da gonbidapena baliogabetu. Zerbitzariak une bateko arazoren bat izan lezake edo agian ez duzu gonbidapena baliogabetzeko baimen nahiko.", + "reacted with %(shortName)s": " erabiltzaileak %(shortName)s batekin erreakzionatu du", + "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.": "Arazoa ikertzen lagundu gaitzakeen testuinguru gehiago badago, esaterako gertatutakoan zer egiten ari zinen, gelaren ID-a, erabiltzaile ID-ak eta abar, mesedez jarri horiek hemen.", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Saioaren datu batzuk, zifratutako mezuen gakoak barne, falta dira. Amaitu saioa eta hasi saioa berriro hau konpontzeko, gakoak babes-kopiatik berreskuratuz.", + "Your browser likely removed this data when running low on disk space.": "Ziur asko zure nabigatzaileak kendu ditu datu hauek diskoan leku gutxi zuelako.", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Fitxategi hau handiegia da igo ahal izateko. Fitxategiaren tamaina-muga %(limit)s da, baina fitxategi honen tamaina %(sizeOfThisFile)s da.", + "These files are too large to upload. The file size limit is %(limit)s.": "Fitxategi hauek handiegiak dira igotzeko. Fitxategien tamaina-muga %(limit)s da.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Fitxategi batzuk handiegiak dira igotzeko. Fitxategien tamaina-muga %(limit)s da.", + "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.": "%(widgetUrl)s helbidean kokatutako trepeta batek zure identitatea egiaztatu nahi du. Hau baimentzen baduzu, trepetak zure erabiltzaile ID-a egiaztatu ahal izango du, baina ez zure izenean ekintzarik egin.", + "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot-ek huts egin du zure hasiera-zerbitzariaren protokoloen zerrenda eskuratzean. Agian hasiera-zerbitzaria zaharregia da hirugarrengoen sareak onartzeko.", + "Failed to get autodiscovery configuration from server": "Huts egin du aurkikuntza automatikoaren konfigurazioa zerbitzaritik eskuratzean", + "Invalid base_url for m.homeserver": "Baliogabeko base_url m.homeserver zerbitzariarentzat", + "Invalid base_url for m.identity_server": "Baliogabeko base_url m.identity_server zerbitzariarentzat", + "Log in to your new account.": "Hasi saioa zure kontu berrian.", + "You can now close this window or log in to your new account.": "Itxi leiho hau edo hasi saioa zure kontu berrian.", + "Registration Successful": "Ongi erregistratuta", + "Upload all": "Igo denak" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index fbcc3d2430..21b99da142 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -58,8 +58,8 @@ "Attachment": "Liite", "Autoplay GIFs and videos": "Toista GIF-animaatiot ja videot automaattisesti", "%(senderName)s banned %(targetName)s.": "%(senderName)s antoi porttikiellon käyttäjälle %(targetName)s.", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Yhdistäminen kotipalvelimeen epäonnistui. Ole hyvä ja tarkista verkkoyhteytesi ja varmista että kotipalvelimen SSL-sertifikaatti on luotettu, ja että jokin selaimen lisäosa ei estä pyyntöjen lähettämisen.", - "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Yhdistäminen kotipalveluun HTTP:n avulla ei ole mahdollista kun selaimen osoitepalkissa on HTTPS URL. Käytä joko HTTPS tai salli turvattomat skriptit.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Kotipalvelimeen ei saada yhteyttä. Tarkista verkkoyhteytesi, varmista että kotipalvelimesi SSL-sertifikaatti on luotettu, ja että mikään selaimen lisäosa ei estä pyyntöjen lähettämistä.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Yhdistäminen kotipalvelimeen HTTP:n avulla ei ole mahdollista, kun selaimen osoitepalkissa on HTTPS-osoite. Käytä joko HTTPS:ää tai salli turvattomat skriptit.", "Can't load user settings": "Käyttäjäasetusten lataaminen epäonnistui", "Change Password": "Muuta salasana", "%(senderName)s changed their profile picture.": "%(senderName)s muutti profiilikuvansa.", @@ -527,7 +527,7 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Huoneen aikajanan tietty hetki yritettiin ladata, mutta sitä ei löytynyt.", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Ei voida varmistaa että osoite, johon tämä kutsu lähetettiin, vastaa tiliisi liittettyä osoitetta.", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (oikeustaso %(powerLevelNumber)s)", - "Verification Pending": "Varmennus on vireillä", + "Verification Pending": "Varmennus odottaa", "(could not connect media)": "(mediaa ei voitu yhdistää)", "WARNING: Device already verified, but keys do NOT MATCH!": "VAROITUS: Laite on jo varmennettu mutta avaimet eivät vastaa toisiaan!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "VAROITUS: AVAIMEN VARMENNUS EPÄONNISTUI! Käyttäjän %(userId)s ja laitteen %(deviceId)s allekirjoitusavain on \"%(fprint)s\" joka ei vastaa annettua avainta \"%(fingerprint)s\". Tämä saattaa tarkoittaa että viestintäsi siepataan!", @@ -583,7 +583,7 @@ "Username available": "Käyttäjätunnus saatavilla", "Username not available": "Käyttäjätunnus ei ole saatavissa", "Something went wrong!": "Jokin meni vikaan!", - "This will be your account name on the homeserver, or you can pick a different server.": "Tästä tulee tilisi nimi -kotipalvelimella, tai voit valita toisen palvelimen.", + "This will be your account name on the homeserver, or you can pick a different server.": "Tästä tulee tilisi nimi -kotipalvelimella, tai voit valita toisen palvelimen.", "If you already have a Matrix account you can log in instead.": "Jos sinulla on jo Matrix-tili, voit kirjautua.", "Your browser does not support the required cryptography extensions": "Selaimesi ei tue vaadittuja kryptografisia laajennuksia", "Not a valid Riot keyfile": "Ei kelvollinen Riot-avaintiedosto", @@ -661,7 +661,7 @@ "Ban this user?": "Anna porttikielto tälle käyttäjälle?", "Unignore": "Huomioi käyttäjä jälleen", "Ignore": "Jätä käyttäjä huomioimatta", - "Jump to read receipt": "Hyppää lukukuittakseen", + "Jump to read receipt": "Hyppää lukukuittaukseen", "Mention": "Mainitse", "Invite": "Kutsu", "User Options": "Käyttäjä-asetukset", @@ -749,7 +749,7 @@ "Community ID": "Yhteisötunniste", "example": "esimerkki", "Advanced options": "Lisäasetukset", - "Block users on other matrix homeservers from joining this room": "Salli vain tämän palvelimen käyttäjät", + "Block users on other matrix homeservers from joining this room": "Salli vain tämän kotipalvelimen käyttäjät", "This setting cannot be changed later!": "Tätä asetusta ei voi muuttaa myöhemmin!", "Add rooms to the community summary": "Lisää huoneita yhteisön yhteenvetoon", "Which rooms would you like to add to this summary?": "Mitkä huoneet haluaisit lisätä tähän yhteenvetoon?", @@ -816,9 +816,9 @@ "Unignored user": "Sallitut käyttäjät", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s muutti %(powerLevelDiffText)s:n oikeustasoa.", - "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s pienoisohjelmaa muokannut %(senderName)s", - "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s pienoisohjelman lisännyt %(senderName)s", - "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s pienoisohjelman poistanut %(senderName)s", + "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s muutti pienoisohjelmaa %(widgetName)s", + "%(widgetName)s widget added by %(senderName)s": "%(senderName)s lisäsi pienoisohjelman %(widgetName)s", + "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s poisti pienoisohjelman %(widgetName)s", "Send": "Lähetä", "Delete %(count)s devices|other": "Poista %(count)s laitetta", "Delete %(count)s devices|one": "Poista laite", @@ -1101,7 +1101,7 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s poisti osoitteet %(removedAddresses)s tältä huoneelta.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s asetti tälle huoneelle pääosoitteen %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s poisti tämän huoneen pääosoitteen.", - "Please contact your service administrator to continue using the service.": "Otathan yhteyttä palvelun ylläpitäjään jatkaaksesi palvelun käyttöä.", + "Please contact your service administrator to continue using the service.": "Ota yhteyttä palvelun ylläpitäjään jatkaaksesi palvelun käyttöä.", "Unable to connect to Homeserver. Retrying...": "Kotipalvelimeen ei saatu yhteyttä. Yritetään uudelleen...", "User %(user_id)s does not exist": "Käyttäjää %(user_id)s ei ole olemassa", "Avoid repeated words and characters": "Vältä toistettuja sanoja ja merkkejä", @@ -1212,7 +1212,7 @@ "Log out and remove encryption keys?": "Kirjaudutaanko ulos ja poistetaan salausavaimet?", "Encrypted": "Salattu", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Muutokset historian lukuoikeuksiin pätevät vain tuleviin viesteihin tässä huoneessa. Nykyisen historian näkyvyys pysyy muuttumattomana.", - "Open Devtools": "Avaa Devtools", + "Open Devtools": "Avaa kehittäjätyökalut", "You don't currently have any stickerpacks enabled": "Sinulla ei ole tarrapaketteja käytössä", "Stickerpack": "Tarrapaketti", "Hide Stickers": "Piilota tarrat", @@ -1229,7 +1229,7 @@ "Theme": "Teema", "Default theme": "Oletusteema", "Account management": "Tilin hallinta", - "Composer": "Lähetys", + "Composer": "Viestin kirjoitus", "Preferences": "Valinnat", "Voice & Video": "Ääni ja video", "Help & About": "Ohje ja tietoja", @@ -1451,7 +1451,7 @@ "Share User": "Jaa käyttäjä", "Share Community": "Jaa yhteisö", "Share Room Message": "Jaa huoneviesti", - "Use a longer keyboard pattern with more turns": "Käytä pidempiä näppäinyhdistelmiä suuremmalla vuoromäärällä", + "Use a longer keyboard pattern with more turns": "Käytä pidempiä näppäinyhdistelmiä, joissa on enemmän suunnanmuutoksia", "Changes your display nickname in the current room only": "Vaihtaa näyttönimesi vain nykyisessä huoneessa", "Group & filter rooms by custom tags (refresh to apply changes)": "Ryhmittele ja suodata huoneita tagien perusteella (päivitä ottaaksesi muutokset käyttöön)", "Render simple counters in room header": "Näytä yksinkertaiset laskurit huoneen yläpalkissa", @@ -1550,7 +1550,7 @@ "Ban users": "Estä käyttäjiä", "Remove messages": "Poista viestejä", "Notify everyone": "Kiinnitä kaikkien huomio", - "Send %(eventType)s events": "Lähetä %(eventType)s tapahtumaa", + "Send %(eventType)s events": "Lähetä %(eventType)s-tapahtumat", "Select the roles required to change various parts of the room": "Valitse roolit, jotka vaaditaan huoneen eri osioiden vaihtamiseen", "Enable encryption?": "Ota salaus käyttöön?", "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.": "Salausta ei voi ottaa pois käytöstä käyttöönoton jälkeen. Viestejä, jotka on lähetetty salattuun huoneeseen, voidaan lukea vain huoneen jäsenten, ei palvelimen, toimesta. Salauksen käyttöönotto saattaa haitata bottien ja siltojen toimivuutta. Lisää tietoa salauksesta.", @@ -1733,7 +1733,7 @@ "Guest access is disabled on this homeserver.": "Tämä kotipalvelin ei salli vieraiden pääsyä.", "Failed to perform homeserver discovery": "Kotipalvelimen etsinnän suoritus epäonnistui", "Unknown failure discovering homeserver": "Kotipalvelimen etsinnässä tapahtui tuntematon virhe", - "This homeserver doesn't offer any login flows which are supported by this client.": "Tämä kotipalvelin ei tarjoa yhtään kirjautumistapaa, jota tämä klientti tukisi.", + "This homeserver doesn't offer any login flows which are supported by this client.": "Tämä kotipalvelin ei tarjoa yhtään kirjautumistapaa, jota tämä asiakasohjelma tukisi.", "Claimed Ed25519 fingerprint key": "Väitetty Ed25519-avaimen sormenjälki", "Set up with a Recovery Key": "Ota palautusavain käyttöön", "Please enter your passphrase a second time to confirm.": "Syötä salalauseesi toisen kerran varmistukseksi.", @@ -1864,5 +1864,39 @@ "Create your Matrix account on ": "Luo Matrix-tili palvelimelle ", "Add room": "Lisää huone", "Your profile": "Oma profiilisi", - "Your Matrix account on ": "Matrix-tilisi palvelimella " + "Your Matrix account on ": "Matrix-tilisi palvelimella ", + "Cannot reach homeserver": "Kotipalvelinta ei voida tavoittaa", + "Your Riot is misconfigured": "Riotin asetukset ovat pielessä", + "Cannot reach identity server": "Identiteettipalvelinta ei voida tavoittaa", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Varmista, että internet-yhteytesi on vakaa, tai ota yhteyttä palvelimen ylläpitäjään", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Pyydä Riot-ylläpitäjääsi tarkistamaan, onko asetuksissasivirheellisiä tai toistettuja merkintöjä.", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Voit rekisteröityä, mutta osa toiminnoista on pois käytöstä kunnes identiteettipalvelin on jälleen toiminnassa. Jos tämä varoitus toistuu, tarkista asetuksesi tai ota yhteyttä palvelimen ylläpitäjään.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Voit palauttaa salasanasi, mutta osa toiminnoista on pois käytöstä kunnes identiteettipalvelin on jälleen toiminnassa. Jos tämä varoitus toistuu, tarkista asetuksesi tai ota yhteyttä palvelimen ylläpitäjään.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Voit kirjautua, mutta osa toiminnoista on pois käytöstä kunnes identiteettipalvelin on jälleen toiminnassa. Jos tämä varoitus toistuu, tarkista asetuksesi tai ota yhteyttä palvelimen ylläpitäjään.", + "Unexpected error resolving identity server configuration": "Odottamaton virhe selvitettäessä identiteettipalvelimen asetuksia", + "Show recently visited rooms above the room list": "Näytä hiljattain vieraillut huoneet huonelistan yläpuolella", + "Low bandwidth mode": "Matalan kaistanleveyden tila", + "No integrations server configured": "Integraatiopalvelinta ei ole määritetty", + "This Riot instance does not have an integrations server configured.": "Tälle Riot-instanssille ei ole määritetty integraatiopalvelinta.", + "Connecting to integrations server...": "Yhdistetään integraatiopalvelimelle...", + "Cannot connect to integrations server": "Integraatiopalvelimeen ei saada yhteyttä", + "The integrations server is offline or it cannot reach your homeserver.": "Integraatiopalvelin on pois toiminnasta tai ei saa yhteyttä kotipalvelimeesi.", + "Unnamed microphone": "Nimetön mikrofoni", + "Unnamed audio output": "Nimetön äänilähtö", + "Unnamed camera": "Nimetön kamera", + "Uploaded sound": "Ladattu ääni", + "Sounds": "Äänet", + "Notification sound": "Ilmoitusääni", + "Reset": "Palauta alkutilaan", + "Set a new custom sound": "Aseta uusi mukautettu ääni", + "Browse": "Selaa", + "Failed to connect to integrations server": "Integraatiopalvelimelle yhdistäminen epäonnistui", + "No integrations server is configured to manage stickers with": "Tarrojen hallintaa varten ei ole määritetty integraatiopalvelinta", + "Use lowercase letters, numbers, dashes and underscores only": "Käytä ainoastaan pieniä kirjaimia, numeroita, ajatusviivoja ja alaviivoja", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Uusi tilisi (%(newAccountId)s) on rekisteröity, mutta olet jo kirjautuneena toisella tilillä (%(loggedInUserId)s).", + "Continue with previous account": "Jatka aiemmalla tilillä", + "Sign out of previous account": "Kirjaudu ulos aiemmasta tilistä", + "Log in to your new account.": "Kirjaudu uudelle tilillesi.", + "You can now close this window or log in to your new account.": "Voit nyt sulkea tämän ikkunan tai kirjautua uudelle tilillesi.", + "Registration Successful": "Rekisteröityminen onnistui" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c5b010b1e4..709610cf96 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1999,5 +1999,22 @@ "Cannot reach identity server": "Impossible de joindre le serveur d’identité", "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Vous pouvez vous inscrire, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur.", "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.": "Vous pouvez réinitialiser votre mot de passe, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur.", - "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.": "Vous pouvez vous connecter, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur." + "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.": "Vous pouvez vous connecter, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur.", + "Log in to your new account.": "Connectez-vous à votre nouveau compte.", + "You can now close this window or log in to your new account.": "Vous pouvez à présent fermer cette fenêtre ou vous connecter à votre nouveau compte.", + "Registration Successful": "Inscription réussie", + "No integrations server configured": "Aucun serveur d’intégrations configuré", + "This Riot instance does not have an integrations server configured.": "Cette instance de Riot n’a aucun serveur d’intégrations configuré.", + "Connecting to integrations server...": "Connexion au serveur d’intégrations…", + "Cannot connect to integrations server": "Impossible de se connecter au serveur d’intégrations", + "The integrations server is offline or it cannot reach your homeserver.": "Le serveur d’intégrations est hors ligne ou il ne peut pas joindre votre serveur d’accueil.", + "Unnamed microphone": "Microphone sans nom", + "Unnamed audio output": "Sortie audio sans nom", + "Unnamed camera": "Caméra sans nom", + "Failed to connect to integrations server": "Échec de la connexion au serveur d’intégrations", + "No integrations server is configured to manage stickers with": "Aucun serveur d’intégrations n’est configuré pour gérer les stickers", + "Upload all": "Tout envoyer", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Votre nouveau compte (%(newAccountId)s) est créé, mais vous êtes déjà connecté avec un autre compte (%(loggedInUserId)s).", + "Continue with previous account": "Continuer avec le compte précédent", + "Sign out of previous account": "Se déconnecter du compte précédent" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3c9deaa0c6..edae9eb6be 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1995,5 +1995,22 @@ "Set a new custom sound": "Új egyedi hang beállítása", "Browse": "Böngész", "Use lowercase letters, numbers, dashes and underscores only": "Csak kisbetűt, számokat, kötőjeleket és aláhúzásokat használj", - "Cannot reach identity server": "Az azonosítási szerver nem érhető el" + "Cannot reach identity server": "Az azonosítási szerver nem érhető el", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Tudsz regisztrálni, de néhány funkció nem lesz elérhető amíg az azonosítási szerver újra elérhető lesz. Ha ezt a figyelmeztetést folyamatosan látod, ellenőrizd a beállításokat vagy vedd fel a kapcsolatot a szerver adminisztrátorával.", + "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.": "A jelszavadat újra beállíthatod, de néhány funkció nem lesz elérhető amíg az azonosítási szerver újra elérhető lesz. Ha ezt a figyelmeztetést folyamatosan látod, ellenőrizd a beállításokat vagy vedd fel a kapcsolatot a szerver adminisztrátorával.", + "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.": "Beléphetsz, de néhány funkció nem lesz elérhető amíg az azonosítási szerver újra elérhető lesz. Ha ezt a figyelmeztetést folyamatosan látod, ellenőrizd a beállításokat vagy vedd fel a kapcsolatot a szerver adminisztrátorával.", + "Log in to your new account.": "Belépés az új fiókodba.", + "You can now close this window or log in to your new account.": "Ezt az ablakot bezárhatod vagy beléphetsz az új fiókodba.", + "Registration Successful": "Regisztráció sikeres", + "No integrations server configured": "Integrációs szerver nincs beállítva", + "This Riot instance does not have an integrations server configured.": "Ennek a Riot kliensnek nincs beállítva integrációs szerver.", + "Connecting to integrations server...": "Integrációs szerverhez csatlakozás...", + "Cannot connect to integrations server": "Az integrációs szerverhez nem lehet kapcsolódni", + "The integrations server is offline or it cannot reach your homeserver.": "Az integrációs szerver nem működik vagy nem tudja elérni a matrix szerveredet.", + "Unnamed microphone": "Név nélküli mikrofon", + "Unnamed audio output": "Név nélküli hang kimenet", + "Unnamed camera": "Név nélküli kamera", + "Failed to connect to integrations server": "Az integrációs szerverhez nem sikerült csatlakozni", + "No integrations server is configured to manage stickers with": "Nincs integrációs szerver konfigurálva amivel a matricákat lehetne kezelni", + "Upload all": "Mindet feltölt" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index bc85aaa387..7b6e3a61aa 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -1876,7 +1876,7 @@ "Try again later, or ask a room admin to check if you have access.": "Riprova più tardi, o chiedi ad un admin della stanza di controllare se hai l'accesso.", "%(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.": "%(errcode)s si è verificato tentando di accedere alla stanza. Se pensi che tu stia vedendo questo messaggio per errore, invia una segnalazione di errore.", "This room has already been upgraded.": "Questa stanza è già stata aggiornata.", - "Agree or Disagree": "", + "Agree or Disagree": "D'accordo o Non d'accordo", "reacted with %(shortName)s": "ha reagito con %(shortName)s", "Edited at %(date)s": "Modificato il %(date)s", "edited": "modificato", @@ -1942,5 +1942,33 @@ "Notification sound": "Suoni di notifica", "Reset": "Ripristina", "Set a new custom sound": "Imposta un nuovo suono personalizzato", - "Browse": "Sfoglia" + "Browse": "Sfoglia", + "Cannot reach homeserver": "Impossibile raggiungere l'homeserver", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Assicurati di avere una connessione internet stabile, o contatta l'amministratore del server", + "Your Riot is misconfigured": "Il tuo Riot è configurato male", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Chiedi al tuo amministratore di Riot di controllare la tua configurazione per voci non valide o doppie.", + "Cannot reach identity server": "Impossibile raggiungere il server identità", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Puoi registrarti, ma alcune funzioni non saranno disponibili finchè il server identità non sarà tornato online. Se continui a vedere questo avviso, controlla la tua configurazione o contatta un amministratore del server.", + "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.": "Puoi ripristinare la password, ma alcune funzioni non saranno disponibili finchè il server identità non sarà tornato online. Se continui a vedere questo avviso, controlla la tua configurazione o contatta un amministratore del server.", + "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.": "Puoi accedere, ma alcune funzioni non saranno disponibili finchè il server identità non sarà tornato online. Se continui a vedere questo avviso, controlla la tua configurazione o contatta un amministratore del server.", + "Unexpected error resolving identity server configuration": "Errore inaspettato risolvendo la configurazione del server identità", + "Like or Dislike": "Piace o Non piace", + "Use lowercase letters, numbers, dashes and underscores only": "Usa solo minuscole, numeri, trattini e trattini bassi", + "Upload all": "Invia tutto", + "Log in to your new account.": "Accedi al tuo nuovo account.", + "You can now close this window or log in to your new account.": "Ora puoi chiudere questa finestra o accedere al tuo nuovo account.", + "Registration Successful": "Registrazione riuscita", + "No integrations server configured": "Nessun server di integrazione configurato", + "This Riot instance does not have an integrations server configured.": "Questa istanza di Riot non ha un server di integrazione configurato.", + "Connecting to integrations server...": "Connessione al server di integrazione...", + "Cannot connect to integrations server": "Impossibile connettersi al server di integrazione", + "The integrations server is offline or it cannot reach your homeserver.": "Il server di integrazione è offline o non può raggiungere il tuo homeserver.", + "Unnamed microphone": "Microfono senza nome", + "Unnamed audio output": "Uscita audio senza nome", + "Unnamed camera": "Fotocamera senza nome", + "Failed to connect to integrations server": "Connessione al server di integrazione fallita", + "No integrations server is configured to manage stickers with": "Nessun server di integrazione configurato con cui gestire gli adesivi", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Il tuo nuovo account (%(newAccountId)s) è registrato, ma hai già fatto l'accesso in un account diverso (%(loggedInUserId)s).", + "Continue with previous account": "Continua con l'account precedente", + "Sign out of previous account": "Disconnetti dall'account precedente" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index bcc4cbe75e..e982fffca8 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1898,5 +1898,12 @@ "Your Riot is misconfigured": "Uw Riot is verkeerd geconfigureerd", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Vraag uw Riot-beheerder om uw configuratie na te kijken op onjuiste of duplicate items.", "Unexpected error resolving identity server configuration": "Onverwachte fout bij het oplossen van de identiteitsserverconfiguratie", - "Use lowercase letters, numbers, dashes and underscores only": "Gebruik enkel letters, cijfers, streepjes en underscores" + "Use lowercase letters, numbers, dashes and underscores only": "Gebruik enkel letters, cijfers, streepjes en underscores", + "Cannot reach identity server": "Kan identiteitsserver niet bereiken", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt zich registreren, 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 serverbeheerder.", + "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 zijn 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.", + "Registration Successful": "Registratie geslaagd" } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 77a058a048..71cb0087a2 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -907,7 +907,7 @@ "Enable email notifications": "Włącz powiadomienia e-mailowe", "Event Type": "Typ wydarzenia", "Download this file": "Pobierz plik", - "Pin Message": "Przypnij Wiadomość", + "Pin Message": "Przypnij wiadomość", "Failed to change settings": "Nie udało się zmienić ustawień", "View Community": "Pokaż społeczność", "%(count)s Members|one": "%(count)s Członek", @@ -1461,5 +1461,9 @@ "Developer options": "Opcje programistyczne", "Change main address for the room": "Zmienianie głównego adresu pokoju", "Modify widgets": "Modyfikowanie widżetów", - "Invite users": "Zapraszanie użytkowników" + "Invite users": "Zapraszanie użytkowników", + "There was an error joining the room": "Wystąpił błąd dołączając do pokoju", + "Joining room …": "Dołączanie do pokoju…", + "edited": "edytowane", + "Edit message": "Edytuj wiadomość" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 7a873c5210..686960467e 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -296,7 +296,7 @@ "No more results": "Больше никаких результатов", "No results": "Нет результатов", "OK": "OK", - "Only people who have been invited": "Только приглашенные участники", + "Only people who have been invited": "Только приглашённые участники", "Passwords can't be empty": "Пароли не могут быть пустыми", "%(senderName)s placed a %(callType)s call.": "%(senderName)s начал(а) %(callType)s-звонок.", "Please check your email and click on the link it contains. Once this is done, click continue.": "Проверьте свою электронную почту и нажмите на содержащуюся ссылку. После этого нажмите кнопку Продолжить.", @@ -336,7 +336,7 @@ "The main address for this room is": "Основной адрес этой комнаты", "This email address is already in use": "Этот email уже используется", "This email address was not found": "Этот адрес электронной почты не найден", - "The email address linked to your account must be entered.": "Необходимо ввести адрес электронной почты, связанный с вашей учетной записью.", + "The email address linked to your account must be entered.": "Необходимо ввести адрес электронной почты, связанный с вашей учётной записью.", "The file '%(fileName)s' failed to upload": "Не удалось отправить файл '%(fileName)s'", "The remote side failed to pick up": "Собеседник не ответил на ваш звонок", "This room has no local addresses": "У этой комнаты нет адресов на вашем сервере", @@ -365,7 +365,7 @@ "numbullet": "элемент нумерованного списка", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Не удается подключиться к домашнему серверу через HTTP, так как в адресной строке браузера указан URL HTTPS. Используйте HTTPS или либо включите небезопасные сценарии.", "Dismiss": "Отклонить", - "Custom Server Options": "Выбор другого сервера", + "Custom Server Options": "Параметры другого сервера", "Mute": "Приглушить", "Operation failed": "Сбой операции", "powered by Matrix": "основано на Matrix", @@ -389,7 +389,7 @@ "Guest access is disabled on this Home Server.": "Гостевой доступ отключен на этом сервере.", "Guests cannot join this room even if explicitly invited.": "Посторонние не смогут войти в эту комнату, даже если они будут приглашены.", "Missing Media Permissions, click here to request.": "Отсутствуют разрешения, нажмите для запроса.", - "No media permissions": "Нет разрешенных носителей", + "No media permissions": "Нет разрешённых носителей", "You may need to manually permit Riot to access your microphone/webcam": "Вам необходимо предоставить Riot доступ к микрофону или веб-камере вручную", "Anyone": "Все", "Are you sure you want to leave the room '%(roomName)s'?": "Вы уверены, что хотите покинуть '%(roomName)s'?", @@ -517,7 +517,7 @@ "Error decrypting image": "Ошибка расшифровки изображения", "Error decrypting video": "Ошибка расшифровки видео", "Add an Integration": "Добавить интеграцию", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Вы будете перенаправлены на внешний сайт, чтобы войти в свою учетную запись для использования с %(integrationsUrl)s. Продолжить?", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Вы будете перенаправлены на внешний сайт, чтобы войти в свою учётную запись для использования с %(integrationsUrl)s. Продолжить?", "Removed or unknown message type": "Сообщение удалено или имеет неизвестный тип", "URL Previews": "Предпросмотр содержимого ссылок", "Drop file here to upload": "Перетащите файл сюда для отправки", @@ -552,8 +552,8 @@ "Username available": "Имя пользователя доступно", "Username not available": "Имя пользователя недоступно", "Something went wrong!": "Что-то пошло не так!", - "This will be your account name on the homeserver, or you can pick a different server.": "Это будет имя вашей учетной записи на домашнем сервере, или вы можете выбрать другой сервер.", - "If you already have a Matrix account you can log in instead.": "Если у вас уже есть учетная запись Matrix, вы можете войти.", + "This will be your account name on the homeserver, or you can pick a different server.": "Это будет имя вашей учётной записи на домашнем сервере, или вы можете выбрать другой сервер.", + "If you already have a Matrix account you can log in instead.": "Если у вас уже есть учётная запись Matrix, вы можете войти.", "Home": "Начало", "Accept": "Принять", "Active call (%(roomName)s)": "Текущий вызов (%(roomName)s)", @@ -707,9 +707,9 @@ "Failed to invite the following users to %(groupId)s:": "Не удалось пригласить этих пользователей в %(groupId)s:", "Failed to remove '%(roomName)s' from %(groupId)s": "Не удалось убрать '%(roomName)s' из %(groupId)s", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Вы действительно хотите убрать '%(roomName)s' из %(groupId)s?", - "Jump to read receipt": "Перейти к последнему прочтенному им сообщению", + "Jump to read receipt": "Перейти к последнему прочтённому им сообщению", "Disable big emoji in chat": "Отключить большие Emoji в чате", - "Message Pinning": "Закрепленные сообщения", + "Message Pinning": "Закреплённые сообщения", "Remove avatar": "Удалить аватар", "Failed to invite users to %(groupId)s": "Не удалось пригласить пользователей в %(groupId)s", "Unable to reject invite": "Невозможно отклонить приглашение", @@ -721,15 +721,15 @@ "Failed to add the following users to the summary of %(groupId)s:": "Не удалось добавить следующих пользователей в сводку %(groupId)s:", "Which rooms would you like to add to this summary?": "Какие комнаты вы хотите добавить в эту сводку?", "Room name or alias": "Название комнаты или псевдоним", - "Pinned Messages": "Закрепленные сообщения", - "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) закрепленные в этой комнате сообщения.", + "Pinned Messages": "Закреплённые сообщения", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s изменил(а) закреплённые в этой комнате сообщения.", "Failed to add the following rooms to the summary of %(groupId)s:": "Не удалось добавить следующие комнаты в сводку %(groupId)s:", "Failed to remove the room from the summary of %(groupId)s": "Не удалось удалить комнату из сводки %(groupId)s", "The room '%(roomName)s' could not be removed from the summary.": "Комнату '%(roomName)s' не удалось удалить из сводки.", "Failed to remove a user from the summary of %(groupId)s": "Не удалось удалить пользователя из сводки %(groupId)s", "The user '%(displayName)s' could not be removed from the summary.": "Пользователя '%(displayName)s' не удалось удалить из сводки.", "Light theme": "Светлая тема", - "Dark theme": "Темная тема", + "Dark theme": "Тёмная тема", "Unknown": "Неизвестно", "Failed to add the following rooms to %(groupId)s:": "Не удалось добавить эти комнаты в %(groupId)s:", "Matrix ID": "Matrix ID", @@ -739,7 +739,7 @@ "You have entered an invalid address.": "Введен неправильный адрес.", "Unpin Message": "Открепить сообщение", "Jump to message": "Перейти к сообщению", - "No pinned messages.": "Нет прикрепленных сообщений.", + "No pinned messages.": "Нет прикреплённых сообщений.", "Loading...": "Загрузка...", "Unnamed room": "Комната без названия", "World readable": "Открыта для чтения", @@ -1013,7 +1013,7 @@ "Advanced notification settings": "Дополнительные параметры уведомлений", "Failed to send logs: ": "Не удалось отправить журналы: ", "delete the alias.": "удалить псевдоним.", - "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": "Чтобы вы могли вернуться в свою учётную запись в будущем, вам необходимо задать пароль", "Forget": "Забыть", "#example": "#пример", "Hide panel": "Скрыть панель", @@ -1046,16 +1046,16 @@ "Failed to get protocol list from Home Server": "Не удалось получить список протоколов с домашнего сервера", "Collecting app version information": "Сбор информации о версии приложения", "Delete the room alias %(alias)s and remove %(name)s from the directory?": "Удалить псевдоним комнаты %(alias)s и удалить %(name)s из каталога?", - "This will allow you to return to your account after signing out, and sign in on other devices.": "Это позволит вам вернуться к учетной записи после выхода из системы и войти на других устройствах.", + "This will allow you to return to your account after signing out, and sign in on other devices.": "Это позволит вам вернуться к учётной записи после выхода из системы и войти на других устройствах.", "Keywords": "Ключевые слова", - "Enable notifications for this account": "Включить уведомления для этой учетной записи", + "Enable notifications for this account": "Включить уведомления для этой учётной записи", "Directory": "Каталог", "Invite to this community": "Пригласить в это сообщество", "Search for a room": "Поиск комнаты", - "Messages containing keywords": "Сообщения, содержащие определенные ключевые слова", + "Messages containing keywords": "Сообщения, содержащие определённые ключевые слова", "View Source": "Просмотр источника", "Tuesday": "Вторник", - "Enter keywords separated by a comma:": "Введите ключевые слова, разделенные запятой:", + "Enter keywords separated by a comma:": "Введите ключевые слова, разделённые запятой:", "Search…": "Поиск…", "You have successfully set a password and an email address!": "Вы успешно установили пароль и email!", "Remove %(name)s from the directory?": "Удалить %(name)s из каталога?", @@ -1122,7 +1122,7 @@ "Riot does not know how to join a room on this network": "Riot не знает, как присоединиться к комнате, принадлежащей к этой сети", "Mentions only": "Только при упоминаниях", "Wednesday": "Среда", - "You can now return to your account after signing out, and sign in on other devices.": "Теперь вы сможете вернуться к своей учетной записи после выхода из системы и войти на других устройствах.", + "You can now return to your account after signing out, and sign in on other devices.": "Теперь вы сможете вернуться к своей учётной записи после выхода из системы и войти на других устройствах.", "Enable email notifications": "Включить уведомления на email", "Event Type": "Тип мероприятия", "Download this file": "Скачать файл", @@ -1159,17 +1159,17 @@ "Enable widget screenshots on supported widgets": "Включить скриншоты виджета в поддерживаемых виджетах", "Collapse Reply Thread": "Ответить с цитированием", "Send analytics data": "Отправить данные аналитики", - "Muted Users": "Приглушенные пользователи", + "Muted Users": "Приглушённые пользователи", "Warning: This widget might use cookies.": "Внимание: этот виджет может использовать cookie.", "Terms and Conditions": "Условия и положения", "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Для продолжения использования сервера %(homeserverDomain)s вы должны ознакомиться и принять условия и положения.", "Review terms and conditions": "Просмотр условий и положений", "e.g. %(exampleValue)s": "напр. %(exampleValue)s", "Failed to indicate account erasure": "Не удается удалить учетную запись", - "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.": "Это навсегда сделает вашу учетную запись невозможной для использования. Вы не сможете войти в систему, и никто не сможет перерегистрировать тот же идентификатор пользователя. Это приведет к тому, что ваша учетная запись выйдет из всех комнат, в которые она входит, и будут удалены данные вашей учетной записи с сервера идентификации. Это действие необратимо.", - "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.": "По умолчанию деактивация вашей учетной записи не приведет к удалению всех ваших сообщений. Если вы хотите, чтобы мы удалили ваши сообщения, поставьте отметку в поле ниже.", + "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.": "Это навсегда сделает вашу учётную запись невозможной для использования. Вы не сможете войти в систему, и никто не сможет перерегистрировать тот же идентификатор пользователя. Это приведёт к тому, что ваша учётная запись выйдет из всех комнат, в которые она входит, и будут удалены данные вашей учётной записи с сервера идентификации. Это действие необратимо.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "По умолчанию деактивация вашей учётной записи не приведёт к удалению всех ваших сообщений. Если вы хотите, чтобы мы удалили ваши сообщения, поставьте отметку в поле ниже.", "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Видимость сообщений в Matrix похожа на электронную почту. Удаление ваших сообщений означает, что отправленные вами сообщения не будут видны новым или незарегистрированным пользователям, но зарегистрированные пользователи, у которых уже есть доступ к этим сообщениям, по-прежнему будут иметь доступ к своей копии.", - "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Пожалуйста, удалите все сообщения, которые я отправил, после деактивации учетной записи. (Внимание: будущие пользователи увидят неполный вид разговоров)", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Пожалуйста, удалите все сообщения, которые я отправил, после деактивации учётной записи. (Внимание: будущие пользователи увидят неполный вид разговоров)", "To continue, please enter your password:": "Чтобы продолжить, введите пароль:", "password": "пароль", "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Пожалуйста, помогите улучшить Riot.im, отправляя анонимные данные использования. При этом будут использоваться cookie (ознакомьтесь с нашейПолитикой cookie).", @@ -1281,10 +1281,10 @@ "Sequences like abc or 6543 are easy to guess": "Последовательности типа abc или 6543 легко угадываемы", "Recent years are easy to guess": "Последние года легко угадываемы", "Dates are often easy to guess": "Даты часто легко угадать", - "This is a top-10 common password": "Это топ-10 распространенных паролей", - "This is a top-100 common password": "Это топ-100 распространенных паролей", - "This is a very common password": "Это очень распространенный пароль", - "This is similar to a commonly used password": "Это похоже на распространенный пароль", + "This is a top-10 common password": "Это топ-10 распространённых паролей", + "This is a top-100 common password": "Это топ-100 распространённых паролей", + "This is a very common password": "Это очень распространённый пароль", + "This is similar to a commonly used password": "Это похоже на распространённый пароль", "A word by itself is easy to guess": "Общеупотребительные слова легко угадываемы", "Names and surnames by themselves are easy to guess": "Имена и фамилии легко угадываемые", "Common names and surnames are easy to guess": "Распространённые имена и фамилии легко угадываемы", @@ -1345,7 +1345,7 @@ "Theme": "Тема", "2018 theme": "Тема 2018 года", "Account management": "Управление аккаунтом", - "Deactivating your account is a permanent action - be careful!": "Деактивация вашей учетной записи — это необратимое действие. Будьте осторожны!", + "Deactivating your account is a permanent action - be careful!": "Деактивация вашей учётной записи — это необратимое действие. Будьте осторожны!", "Chat with Riot Bot": "Чат с ботом Riot", "Help & About": "Помощь & о программе", "FAQ": "Часто задаваемые вопросы", @@ -1353,12 +1353,12 @@ "Lazy loading is not supported by your current homeserver.": "Ленивая подгрузка не поддерживается вашим сервером.", "Preferences": "Параметры", "Room list": "Список комнат", - "Timeline": "Временная шкала", + "Timeline": "Временна́я шкала", "Autocomplete delay (ms)": "Задержка автодополнения (мс)", "Roles & Permissions": "Роли и права", "Security & Privacy": "Безопасность и конфиденциальность", "Encryption": "Шифрование", - "Encrypted": "Зашифрованно", + "Encrypted": "Зашифровано", "Ignored users": "Игнорируемые пользователи", "Key backup": "Резервное копирование ключей", "Voice & Video": "Голос и видео", @@ -1392,7 +1392,7 @@ "All keys backed up": "Все ключи сохранены", "Developer options": "Параметры разработчика", "General": "Общий", - "Set a new account password...": "Установить новый пароль учетной записи...", + "Set a new account password...": "Установить новый пароль учётной записи...", "Legal": "Законный", "At this time it is not possible to reply with an emote.": "В настоящее время невозможно ответить с помощью эмоции.", "Room avatar": "Аватар комнаты", @@ -1435,7 +1435,7 @@ "Copy to clipboard": "Скопировать в буфер обмена", "Download": "Скачать", "Your Recovery Key": "Ваш ключ восстановления", - "Create your account": "Создать учетную запись", + "Create your account": "Создать учётную запись", "Your account": "Ваша учетная запись", "Username": "Имя пользователя", "Not sure of your password? Set a new one": "Не уверены в пароле? Установите новый", @@ -1612,7 +1612,7 @@ "Please confirm that you'd like to go forward with upgrading this room from to .": "Пожалуйста, подтвердите, что вы хотите перейти к обновлению этой комнаты с на .", "Upgrade": "Обновление", "Changes your avatar in this current room only": "Меняет ваш аватар только в этой комнате", - "Unbans user with given ID": "Разблокировать пользователя с заданным ID", + "Unbans user with given ID": "Разблокирует пользователя с заданным ID", "Adds a custom widget by URL to the room": "Добавляет пользовательский виджет по URL-адресу в комнате", "Please supply a https:// or http:// widget URL": "Пожалуйста, укажите https:// или http:// адрес URL виджета", "You cannot modify widgets in this room.": "Вы не можете изменять виджеты в этой комнате.", @@ -1662,12 +1662,12 @@ "inline-code": "встроенный код", "block-quote": "цитата", "bulleted-list": "маркированный список", - "numbered-list": "пронумерованный-лист", + "numbered-list": "нумерованный-список", "This room has been replaced and is no longer active.": "Эта комната была заменена и больше не активна.", "Joining room …": "Вступление в комнату …", "Loading …": "Загрузка…", "Rejecting invite …": "Отказ от приглашения …", - "Join the conversation with an account": "Присоединиться к разговору с учетной записью", + "Join the conversation with an account": "Присоединиться к разговору с учётной записью", "Sign Up": "Подписаться", "Sign In": "Войти в систему", "You were kicked from %(roomName)s by %(memberName)s": "Вы были выгнаны %(memberName)s из %(roomName)s", @@ -1678,11 +1678,11 @@ "Something went wrong with your invite to %(roomName)s": "Что-то пошло не так с вашим приглашением в %(roomName)s", "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s был возвращен при попытке подтвердить ваше приглашение. Вы можете попытаться передать эту информацию администратору комнаты.", "You can only join it with a working invite.": "Вы можете присоединиться к ней только с рабочим приглашением.", - "You can still join it because this is a public room.": "Вы все еще можете присоединиться к ней, потому что это общественная комната.", + "You can still join it because this is a public room.": "Вы всё ещё можете присоединиться к ней, потому что это публичная комната.", "Join the discussion": "Присоединяйтесь к обсуждению", "Try to join anyway": "Постарайся присоединиться в любом случае", "This invite to %(roomName)s wasn't sent to your account": "Это приглашение к %(roomName)s не было отправлено на ваш аккаунт", - "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Войдите в систему с другой учетной записью, запросите новое приглашение или добавьте email %(email)s к этой учетной записи.", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Войдите в систему с другой учётной записью, запросите новое приглашение или добавьте email %(email)s к этой учётной записи.", "Do you want to chat with %(user)s?": "Хотите пообщаться с %(user)s?", "Do you want to join %(roomName)s?": "Хотите присоединиться к %(roomName)s?", " invited you": " пригласил тебя", @@ -1695,17 +1695,17 @@ "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Сообщения в этой комнате защищены сквозным шифрованием. Только вы и получатель(и) имеют ключи для чтения этих сообщений.", "Securely back up your keys to avoid losing them. Learn more.": "Надежно сохраните резервную копию ключей, чтобы не потерять их. Подробнее", "Don't ask me again": "Не спрашивай меня больше", - "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Обновление этой комнаты отключит текущий экземпляр комнаты и создаст обновленную комнату с тем же именем.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Обновление этой комнаты отключит текущий экземпляр комнаты и создаст обновлённую комнату с тем же именем.", "This room has already been upgraded.": "Эта комната уже была обновлена.", "This room is running room version , which this homeserver has marked as unstable.": "Эта комната работает под управлением версии комнаты , которую этот сервер пометил как unstable.", "Add some now": "Добавить сейчас", "Failed to revoke invite": "Не удалось отменить приглашение", - "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Не удалось отозвать приглашение. Возможно, на сервере возникла временная проблема или у вас недостаточно прав для отзыва приглашения.", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Не удалось отозвать приглашение. Возможно, на сервере возникла вре́менная проблема или у вас недостаточно прав для отзыва приглашения.", "Revoke invite": "Отозвать приглашение", "Invited by %(sender)s": "Приглашен %(sender)s", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "При обновлении основного адреса комнаты произошла ошибка. Возможно, это не разрешено сервером или произошел временный сбой.", "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.": "Произошла ошибка при создании этого псевдонима. Возможно, это не разрешено сервером или произошёл временный сбой.", "Error removing alias": "Ошибка удаления псевдонима", "There was an error removing that alias. It may no longer exist or a temporary error occurred.": "Произошла ошибка при удалении этого псевдонима. Возможно, он больше не существует или произошла временная ошибка.", "Error updating flair": "Ошибка обновления стиля", @@ -1722,11 +1722,11 @@ "Rotate clockwise": "Повернуть по часовой стрелке", "Edit message": "Редактировать сообщение", "Power level": "Уровень мощности", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не удалось найти профили для Matrix ID, перечисленных ниже. Вы все равно хотите их пригласить?", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не удалось найти профили для Matrix ID, перечисленных ниже. Вы всё равно хотите их пригласить?", "Invite anyway": "Пригласить в любом случае", "GitHub issue": "GitHub вопрос", "Notes": "Заметка", - "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Если есть дополнительный контекст, который может помочь в анализе проблемы, такой как то, что вы делали в то время, ID комнат, ID пользователей и т. Д., пожалуйста, включите эти данные.", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Если есть дополнительный контекст, который может помочь в анализе проблемы, такой как то, что вы делали в то время, ID комнат, ID пользователей и т. д., пожалуйста, включите эти данные.", "Unable to load commit detail: %(msg)s": "Невозможно загрузить детали: %(msg)s", "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": "Чтобы не потерять историю чата, вы должны экспортировать ключи от комнаты перед выходом из системы. Вам нужно будет вернуться к более новой версии Riot, чтобы сделать это", "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. ": "Ранее вы использовали более новую версию Riot на %(host)s. Чтобы снова использовать эту версию с сквозным шифрованием, вам нужно выйти и снова войти. ", @@ -1738,7 +1738,7 @@ "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Проверьте этого пользователя, чтобы отметить его как доверенного. Доверенные пользователи дают вам дополнительное спокойствие при использовании сквозного шифрования сообщений.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Проверка этого пользователя пометит его устройство как доверенное, а также пометит ваше устройство как доверенное.", "Waiting for partner to confirm...": "В ожидании партнера, чтобы подтвердить ...", - "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.": "Ранее вы использовали Riot на %(host)s с отложенной загрузкой участников. В этой версии отложенная загрузка отключена. Поскольку локальный кеш не совместим между этими двумя настройками, Riot необходимо повторно синхронизировать вашу учетную запись.", + "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.": "Ранее вы использовали Riot на %(host)s с отложенной загрузкой участников. В этой версии отложенная загрузка отключена. Поскольку локальный кеш не совместим между этими двумя настройками, Riot необходимо повторно синхронизировать вашу учётную запись.", "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.": "Если другая версия Riot все еще открыта на другой вкладке, закройте ее, так как использование Riot на том же хосте с включенной и отключенной ленивой загрузкой одновременно вызовет проблемы.", "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 теперь использует в 3-5 раз меньше памяти, загружая информацию только о других пользователях, когда это необходимо. Пожалуйста, подождите, пока мы ресинхронизируемся с сервером!", "I don't want my encrypted messages": "Я не хочу, мои зашифрованные сообщения", @@ -1786,13 +1786,13 @@ "Set a new status...": "Установка нового статуса...", "Hide": "Скрыть", "This homeserver would like to make sure you are not a robot.": "Этот сервер хотел бы убедиться, что вы не робот.", - "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.": "Пользовательские параметры сервера можно использовать для входа на другие серверы Matrix, указав другой URL-адрес сервера. Это позволяет использовать это приложение с существующей учетной записью Matrix на другом домашнем сервере.", + "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.": "Пользовательские параметры сервера можно использовать для входа на другие серверы Matrix, указав другой URL-адрес сервера. Это позволяет использовать это приложение с существующей учётной записью Matrix на другом домашнем сервере.", "Please review and accept all of the homeserver's policies": "Пожалуйста, просмотрите и примите все правила сервера", "Please review and accept the policies of this homeserver:": "Пожалуйста, просмотрите и примите политику этого сервера:", "Unable to validate homeserver/identity server": "Невозможно проверить сервер/сервер идентификации", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Введите местоположение вашего Modula homeserver. Он может использовать ваше собственное доменное имя или быть поддоменом modular.im.", - "Sign in to your Matrix account on %(serverName)s": "Войдите в свою учетную запись Matrix на %(serverName)s", - "Sign in to your Matrix account on ": "Войдите в свою учетную запись Matrix с помощью ", + "Sign in to your Matrix account on %(serverName)s": "Войдите в свою учётную запись Matrix на %(serverName)s", + "Sign in to your Matrix account on ": "Войдите в свою учётную запись Matrix с помощью ", "Change": "Изменить", "Use an email address to recover your account": "Используйте email, чтобы восстановить свой аккаунт", "Enter email address (required on this homeserver)": "Введите адрес электронной почты (требуется для этого сервера)", @@ -1808,9 +1808,9 @@ "Use letters, numbers, dashes and underscores only": "Используйте только буквы, цифры, тире и подчеркивание", "Enter username": "Введите имя пользователя", "Some characters not allowed": "Некоторые символы не разрешены", - "Create your Matrix account on %(serverName)s": "Создайте свою учетную запись Matrix на %(serverName)s", - "Create your Matrix account on ": "Создайте свою учетную запись Matrix на ", - "Use an email address to recover your account.": "Для восстановления учетной записи используйте email.", + "Create your Matrix account on %(serverName)s": "Создайте свою учётную запись Matrix на %(serverName)s", + "Create your Matrix account on ": "Создайте свою учётную запись Matrix на ", + "Use an email address to recover your account.": "Для восстановления учётной записи используйте email.", "Other users can invite you to rooms using your contact details.": "Другие пользователи могут пригласить вас в комнаты, используя ваши контактные данные.", "Enter custom server URLs What does this mean?": "Введите пользовательские URL-адреса сервера Что это значит?", "Join millions for free on the largest public server": "Присоединяйтесь бесплатно к миллионам на крупнейшем общедоступном сервере", @@ -1824,7 +1824,7 @@ "Want more than a community? Get your own server": "Хотите больше, чем просто сообщество? Получите свой собственный сервер", "This homeserver does not support communities": "Этот сервер не поддерживает сообщества", "You are logged in to another account": "Вы вошли в другой аккаунт", - "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Спасибо за подтверждение вашей электронной почты! Учетная запись, в которую вы вошли (%(sessionUserId)s), похоже отличается от учетной записи, для которой вы подтвердили адрес электронной почты (%(VerifiedUserId)s). Если вы хотите войти в %(VerifiedUserId2)s, сначала выйдите из системы.", + "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Спасибо за подтверждение вашей электронной почты! Учётная запись, в которую вы вошли (%(sessionUserId)s), похоже отличается от учётной записи, для которой вы подтвердили адрес электронной почты (%(verifiedUserId)s). Если вы хотите войти в %(verifiedUserId2)s, сначала выйдите из системы.", "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot не смог получить список протоколов с сервера.Сервер может быть слишком старым для поддержки сетей сторонних производителей.", "Riot failed to get the public room list.": "Riot не смог получить список публичных комнат.", "The homeserver may be unavailable or overloaded.": "Сервер может быть недоступен или перегружен.", @@ -1836,8 +1836,8 @@ "Your profile": "Ваш профиль", "Could not load user profile": "Не удалось загрузить профиль пользователя", "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Смена пароля приведет к сбросу любых сквозных ключей шифрования на всех ваших устройствах, что сделает историю зашифрованного чата нечитаемой. Настройте Резервное копирование ключей или экспортируйте ключи от вашей комнаты с другого устройства перед сбросом пароля.", - "Your Matrix account on %(serverName)s": "Ваша учетная запись Matrix на %(serverName)s", - "Your Matrix account on ": "Ваша учетная запись Matrix на ", + "Your Matrix account on %(serverName)s": "Ваша учётная запись Matrix на %(serverName)s", + "Your Matrix account on ": "Ваша учётная запись Matrix на ", "A verification email will be sent to your inbox to confirm setting your new password.": "Письмо с подтверждением будет отправлено на ваш почтовый ящик, чтобы подтвердить установку нового пароля.", "Sign in instead": "Войдите, вместо этого", "Your password has been reset.": "Ваш пароль был сброшен.", @@ -1845,7 +1845,7 @@ "Set a new password": "Установить новый пароль", "Invalid homeserver discovery response": "Неверное обнаружения сервера", "Failed to get autodiscovery configuration from server": "Не удалось получить конфигурацию автообнаружения с сервера", - "Show recently visited rooms above the room list": "Показать недавно посещенные комнаты над списком комнат", + "Show recently visited rooms above the room list": "Показать недавно посещённые комнаты над списком комнат", "Invalid base_url for m.homeserver": "Неверный base_url для m.homeserver", "Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес сервера не является действительным URL-адресом сервера Матрица", "Invalid identity server discovery response": "Неверный ответ на запрос идентификации сервера", @@ -1855,12 +1855,12 @@ "This homeserver does not support login using email address.": "Этот сервер не поддерживает вход с использованием адреса электронной почты.", "Failed to perform homeserver discovery": "Не удалось выполнить обнаружение сервера", "Sign in with single sign-on": "Войдите в систему с единой регистрацией", - "Create account": "Создать учетную запись", + "Create account": "Создать учётную запись", "Registration has been disabled on this homeserver.": "Регистрация на этом сервере отключена.", "Unable to query for supported registration methods.": "Невозможно запросить поддерживаемые методы регистрации.", "Great! This passphrase looks strong enough.": "Отлично! Этот пароль выглядит достаточно сильной.", "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "Мы будем хранить зашифрованную копию ваших ключей на нашем сервере. Защитите свою резервную копию паролем, чтобы сохранить ее в безопасности.", - "For maximum security, this should be different from your account password.": "Для максимальной безопасности это должно отличаться от пароля вашей учетной записи.", + "For maximum security, this should be different from your account password.": "Для максимальной безопасности это должно отличаться от пароля вашей учётной записи.", "Enter a passphrase...": "Введите пароль....", "Set up with a Recovery Key": "Настройка с ключом восстановления", "That matches!": "Это совпадает!", @@ -1891,14 +1891,21 @@ "Unable to create key backup": "Невозможно создать резервную копию ключа", "Retry": "Попробуйте снова", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Без настройки безопасного восстановления сообщений при выходе из системы вы потеряете историю защищенных сообщений.", - "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.": "Если вы не установили новый метод восстановления, злоумышленник может попытаться получить доступ к вашей учетной записи. Измените пароль учетной записи и сразу установите новый способ восстановления в настройках.", + "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.": "Если вы не установили новый метод восстановления, злоумышленник может попытаться получить доступ к вашей учётной записи. Измените пароль учётной записи и сразу установите новый способ восстановления в настройках.", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Это устройство обнаружило, что ваш пароль восстановления и ключ для безопасных сообщений были удалены.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "Если вы сделали это случайно, вы можете настроить безопасные сообщения на этом устройстве, которые будут повторно зашифровать историю сообщений этого устройства с новым методом восстановления.", - "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.": "Если вы не удалили метод восстановления, злоумышленник может попытаться получить доступ к вашей учетной записи. Измените пароль учетной записи и сразу установите новый способ восстановления в настройках.", - "Cannot reach homeserver": "Не удается связаться с сервером", + "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.": "Если вы не удалили метод восстановления, злоумышленник может попытаться получить доступ к вашей учётной записи. Измените пароль учётной записи и сразу установите новый способ восстановления в настройках.", + "Cannot reach homeserver": "Не удаётся связаться с сервером", "Ensure you have a stable internet connection, or get in touch with the server admin": "Убедитесь, что у вас есть стабильное подключение к интернету, или свяжитесь с администратором сервера", - "Your Riot is misconfigured": "Твой Riot неправильно настроен", + "Your Riot is misconfigured": "Ваш Riot неправильно настроен", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Попросите администратора Riot проверить конфигурационный файл на наличие неправильных или повторяющихся записей.", - "Unexpected error resolving identity server configuration": "Неопределённая ошибка при разборе опции сервера идентификации", - "Use lowercase letters, numbers, dashes and underscores only": "Используйте только строчные буквы, цифры, тире и подчеркивания" + "Unexpected error resolving identity server configuration": "Неопределённая ошибка при разборе параметра сервера идентификации", + "Use lowercase letters, numbers, dashes and underscores only": "Используйте только строчные буквы, цифры, тире и подчеркивания", + "Cannot reach identity server": "Не удаётся связаться с сервером идентификации", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Вы можете зарегистрироваться, но некоторые возможности не будет доступны, пока сервер идентификации не станет доступным. Если вы продолжаете видеть это предупреждение, проверьте вашу конфигурацию или свяжитесь с администратором сервера.", + "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.": "Вы можете сбросить пароль, но некоторые возможности не будет доступны, пока сервер идентификации не станет доступным. Если вы продолжаете видеть это предупреждение, проверьте вашу конфигурацию или свяжитесь с администратором сервера.", + "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.": "Вы можете войти в систему, но некоторые возможности не будет доступны, пока сервер идентификации не станет доступным. Если вы продолжаете видеть это предупреждение, проверьте вашу конфигурацию или свяжитесь с администратором сервера.", + "Log in to your new account.": "Войти в вашу новую учётную запись.", + "You can now close this window or log in to your new account.": "Вы можете закрыть это окно или войти в вашу новую учётную запись.", + "Registration Successful": "Регистрация успешно завершена" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 6a57569025..3d94d3a670 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1953,5 +1953,26 @@ "Your Riot is misconfigured": "Riot-i juaj është i keqformësuar", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Kërkojini përgjegjësit të Riot-it tuaj të kontrollojë formësimin tuaj për zëra të pasaktë ose të përsëdytur.", "Unexpected error resolving identity server configuration": "Gabim i papritur teksa ftillohej formësimi i shërbyesit të identiteteve", - "Use lowercase letters, numbers, dashes and underscores only": "Përdorni vetëm shkronja të vogla, numra, vija ndarëse dhe nënvija" + "Use lowercase letters, numbers, dashes and underscores only": "Përdorni vetëm shkronja të vogla, numra, vija ndarëse dhe nënvija", + "Cannot reach identity server": "S’kapet dot shërbyesi i identiteteve", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Mund të regjistroheni, por disa veçori do të jenë të papërdorshme, derisa shërbyesi i identiteteve të jetë sërish në linjë. Nëse vazhdoni ta shihni këtë sinjalizim, kontrolloni formësimin tuaj ose lidhuni me një përgjegjës të shërbyesit.", + "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.": "Mund të ricaktoni fjalëkalimin, por disa veçori do të jenë të papërdorshme, derisa shërbyesi i identiteteve të jetë sërish në linjë. Nëse vazhdoni ta shihni këtë sinjalizim, kontrolloni formësimin tuaj ose lidhuni me një përgjegjës të shërbyesit.", + "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.": "Mund të bëni hyrjen, por disa veçori do të jenë të papërdorshme, derisa shërbyesi i identiteteve të jetë sërish në linjë. Nëse vazhdoni ta shihni këtë sinjalizim, kontrolloni formësimin tuaj ose lidhuni me një përgjegjës të shërbyesit.", + "Log in to your new account.": "Bëni hyrjen te llogaria juaj e re.", + "You can now close this window or log in to your new account.": "Tani mund ta mbyllni këtë dritare ose të bëni hyrjen në llogarinë tuaj të re.", + "Registration Successful": "Regjistrim i Suksesshëm", + "No integrations server configured": "S’ka të formësuar shërbyes integrimesh", + "This Riot instance does not have an integrations server configured.": "Kjo instancë Riot-i s’ka të formësuar ndonjë shërbyes integrimesh.", + "Connecting to integrations server...": "Po lidhet te një shërbyes integrimesh…", + "Cannot connect to integrations server": "S’lidhet dot me shërbyes integrimesh", + "The integrations server is offline or it cannot reach your homeserver.": "Shërbyesi i integrimeve s’është në linjë ose s’kap dot shërbyesin tuaj Home.", + "Unnamed microphone": "Mikrofon i paemërtuar", + "Unnamed audio output": "Dalje audio e paemërtuar", + "Unnamed camera": "Kamerë e paemërtuar", + "Failed to connect to integrations server": "S’u arrit të lidhej me shërbyes integrimesh", + "No integrations server is configured to manage stickers with": "S’ka shërbyes integrimesh të formësuar për administrim ngjitësish", + "Upload all": "Ngarkoji krejt", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Llogaria juaj e re (%(newAccountId)s) është e regjistruar, por jeni i futur në një tjetër llogari (%(loggedInUserId)s).", + "Continue with previous account": "Vazhdoni me llogarinë e mëparshme", + "Sign out of previous account": "Dilni nga llogaria e mëparshme" } diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json new file mode 100644 index 0000000000..d3cfea2f49 --- /dev/null +++ b/src/i18n/strings/vi.json @@ -0,0 +1,9 @@ +{ + "This email address is already in use": "Email này hiện đã được sử dụng", + "This phone number is already in use": "Số điện thoại này hiện đã được sử dụng", + "Failed to verify email address: make sure you clicked the link in the email": "Xác thực email thất bại: hãy đảm bảo bạn nhấp đúng đường dẫn đã gửi vào email", + "The platform you're on": "Nền tảng bạn đang tham gia", + "The version of Riot.im": "Phiên bản của Riot.im", + "Your language of choice": "Ngôn ngữ bạn chọn", + "Your homeserver's URL": "Đường dẫn Homeserver của bạn" +} diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index b06f5b9fdd..d208812832 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -166,7 +166,7 @@ "You are no longer ignoring %(userId)s": "Je negeert %(userId)s nie mi", "Define the power level of a user": "Bepoal ’t machtsniveau van e gebruuker", "Deops user with given id": "Ountmachtigt de gebruuker me de gegeevn ID", - "Opens the Developer Tools dialog": "Opent ’t dialoogvenster me ’t ountwikkeloarsgereedschap", + "Opens the Developer Tools dialog": "Opent de dialoogveinster me ’t ountwikkeloarsgereedschap", "Adds a custom widget by URL to the room": "Voegt met een URL een angepaste widget toe an ’t gesprek", "Please supply a https:// or http:// widget URL": "Gift een https://- of http://-widget-URL in", "You cannot modify widgets in this room.": "J’en kut de widgets in dit gesprek nie anpassn.", @@ -577,7 +577,7 @@ "Labs": "Experimenteel", "Notifications": "Meldiengn", "Start automatically after system login": "Automatisch startn achter systeemanmeldienge", - "Close button should minimize window to tray": "Venster minimaliseern noa ’t systeemvak by ’t sluutn", + "Close button should minimize window to tray": "Veinster minimaliseern noa ’t systeemvak by ’t sluutn", "Preferences": "Instelliengn", "Composer": "Ipsteller", "Timeline": "Tydslyn", @@ -622,7 +622,7 @@ "Developer options": "Ontwikkeloarsopties", "Open Devtools": "Ontwikkelgereedschap openn", "Room Addresses": "Gespreksadressn", - "Publish this room to the public in %(domain)s's room directory?": "Dit gesprek openboar moakn in de gesprekscatalogus van %(domain)s?", + "Publish this room to the public in %(domain)s's room directory?": "Dit gesprek openboar moakn in de gesprekscataloog van %(domain)s?", "URL Previews": "URL-voorvertoniengn", "Change room avatar": "Gespreksavatar wyzign", "Change room name": "Gespreksnoame wyzign", @@ -984,7 +984,7 @@ "Minimize apps": "Apps minimaliseern", "Maximize apps": "Apps maximaliseern", "Reload widget": "Widget herloadn", - "Popout widget": "Widget in nieuw venster openn", + "Popout widget": "Widget in e nieuwe veinster openn", "Picture": "Fotootje", "Revoke widget access": "Widget-toegank intrekkn", "Create new room": "E nieuw gesprek anmoakn", @@ -1192,7 +1192,7 @@ "Refresh": "Herloadn", "Unable to restore session": "’t En is nie meuglik van de sessie t’herstelln", "We encountered an error trying to restore your previous session.": "’t Is e foute ipgetreedn by ’t herstelln van je vorige sessie.", - "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "A j’al e ki gebruuk gemakt èt van e recentere versie van Riot, is je sessie meugliks ounverenigboar me deze versie. Sluut dit venster en goa were noa de recentere versie.", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "A j’al e ki gebruuk gemakt èt van e recentere versie van Riot, is je sessie meugliks ounverenigboar me deze versie. Sluut deze veinster en goa were noa de recentere versie.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "’t Legen van den ipslag van je browser goa ’t probleem misschiens verhelpn, mo goa joun ook afmeldn en gans je versleuterde gespreksgeschiedenisse ounleesboar moakn.", "Verification Pending": "Verificoatie in afwachtienge", "Please check your email and click on the link it contains. Once this is done, click continue.": "Bekyk jen e-mails en klikt ip de koppelienge derin. Klikt van zodra da je da gedoan èt ip ‘Verdergoan’.", @@ -1308,7 +1308,7 @@ "This homeserver would like to make sure you are not a robot.": "Deze thuusserver wil geirn weetn of da je gy geen robot zyt.", "Custom Server Options": "Angepaste serverinstelliengn", "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.": "Je kut de angepaste serveropties gebruukn vo jen an te meldn by andere Matrix-servers, deur een andere thuusserver-URL in te geevn. Dit biedt je de meuglikheid vo deze toepassienge te gebruukn met een bestoande Matrix-account ip een andere thuusserver.", - "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Je kut ook een angepaste identiteitsserver instelln, ma je goa geen gebruukers kunn uutnodign via e-mail, of zelve via e-mail uutgenodigd wirden.", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Je kut ook een angepasten identiteitsserver instelln, ma je goa geen gebruukers kunn uutnodign via e-mail, of zelve via e-mail uutgenodigd wirden.", "To continue, please enter your password.": "Gif je paswoord in vo verder te goan.", "Password:": "Paswoord:", "Please review and accept all of the homeserver's policies": "Gelieve ’t beleid van de thuusserver te leezn en ’anveirdn", @@ -1646,5 +1646,12 @@ "Your Riot is misconfigured": "Je Riot is verkeerd geconfigureerd gewist", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Vroagt an je Riot-beheerder van je configuroatie noa te kykn ip verkeerde of duplicoate items.", "Unexpected error resolving identity server configuration": "Ounverwachte foute by ’t iplossn van d’identiteitsserverconfiguroatie", - "Use lowercase letters, numbers, dashes and underscores only": "Gebruukt alleene moa letters, cyfers, streeptjes en underscores" + "Use lowercase letters, numbers, dashes and underscores only": "Gebruukt alleene moa letters, cyfers, streeptjes en underscores", + "Cannot reach identity server": "Kostege den identiteitsserver nie bereikn", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku je registreern, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e serverbeheerder.", + "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.": "Je ku je paswoord herinstelln, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e 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.": "Je ku jen anmeldn, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e serverbeheerder.", + "Log in to your new account.": "Meldt jen eigen an me je nieuwen account.", + "You can now close this window or log in to your new account.": "Je kut deze veinster nu sluutn, of jen eigen anmeldn me je nieuwen account.", + "Registration Successful": "Registroatie gesloagd" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 31e370abed..3d9b570dfe 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1993,5 +1993,22 @@ "Cannot reach identity server": "無法連線至身份識別伺服器", "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以註冊,但有些功能在身份識別伺服器重新上線前會沒辦法運作。如果您一直看到這個警告,請檢查您的設定或聯絡伺服器管理員。", "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.": "您可以重設密碼,但有些功能在身份識別伺服器重新上線前會沒辦法運作。如果您一直看到這個警告,請檢查您的設定或聯絡伺服器管理員。", - "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.": "您可以登入,但有些功能在身份識別伺服器重新上線前會沒辦法運作。如果您一直看到這個警告,請檢查您的設定或聯絡伺服器管理員。" + "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.": "您可以登入,但有些功能在身份識別伺服器重新上線前會沒辦法運作。如果您一直看到這個警告,請檢查您的設定或聯絡伺服器管理員。", + "Log in to your new account.": "登入到您的新帳號。", + "You can now close this window or log in to your new account.": "您現在可以關閉此視窗或登入到您的新帳號了。", + "Registration Successful": "註冊成功", + "Upload all": "上傳全部", + "No integrations server configured": "沒有設定好的整合伺服器", + "This Riot instance does not have an integrations server configured.": "這個 Riot 站台沒有設定整合伺服器。", + "Connecting to integrations server...": "正在連線到整合伺服器……", + "Cannot connect to integrations server": "無法連線到整合伺服器", + "The integrations server is offline or it cannot reach your homeserver.": "整合伺服器已離線或無法連線到您的家伺服器。", + "Unnamed microphone": "未命名的麥克風", + "Unnamed audio output": "未命名的音訊輸出", + "Unnamed camera": "未命名的攝影機", + "Failed to connect to integrations server": "連線到整合伺服器失敗", + "No integrations server is configured to manage stickers with": "未設定整合伺服器來管理貼圖", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "您的新帳號 %(newAccountId)s 已註冊,但您已經登入到不同的帳號 (%(loggedInUserId)s)。", + "Continue with previous account": "使用先前的帳號繼續", + "Sign out of previous account": "登出先前的帳號" } diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 5ecb91b305..7a98c0dba6 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -46,8 +46,8 @@ export default function shouldHideEvent(ev) { // Hide redacted events if (ev.isRedacted() && !isEnabled('showRedactions')) return true; - // Hide replacement events since they update the original tile - if (ev.isRelation("m.replace")) return true; + // Hide replacement events since they update the original tile (if enabled) + if (ev.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return true; const eventDiff = memberEventDiff(ev); diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 06823e5d2a..e83e0348ca 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -67,7 +67,7 @@ export default class AutoDiscoveryUtils { {}, { a: (sub) => { return {sub}; diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 219b53bc5e..ffc47e2277 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -46,9 +46,12 @@ export function isContentActionable(mxEvent) { } export function canEditContent(mxEvent) { - return mxEvent.status !== EventStatus.CANCELLED && - mxEvent.getType() === 'm.room.message' && - mxEvent.getOriginalContent().msgtype === "m.text" && + if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message") { + return false; + } + const content = mxEvent.getOriginalContent(); + const {msgtype} = content; + return (msgtype === "m.text" || msgtype === "m.emote") && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } diff --git a/src/utils/pillify.js b/src/utils/pillify.js new file mode 100644 index 0000000000..e943cfe657 --- /dev/null +++ b/src/utils/pillify.js @@ -0,0 +1,118 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import ReactDOM from 'react-dom'; +import MatrixClientPeg from '../MatrixClientPeg'; +import SettingsStore from "../settings/SettingsStore"; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; +import sdk from '../index'; + +export function pillifyLinks(nodes, mxEvent) { + const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); + let node = nodes[0]; + while (node) { + let pillified = false; + + if (node.tagName === "A" && node.getAttribute("href")) { + const href = node.getAttribute("href"); + + // If the link is a (localised) matrix.to link, replace it with a pill + const Pill = sdk.getComponent('elements.Pill'); + if (Pill.isMessagePillUrl(href)) { + const pillContainer = document.createElement('span'); + + const pill = ; + + ReactDOM.render(pill, pillContainer); + node.parentNode.replaceChild(pillContainer, node); + // Pills within pills aren't going to go well, so move on + pillified = true; + + // update the current node with one that's now taken its place + node = pillContainer; + } + } else if ( + node.nodeType === Node.TEXT_NODE && + // as applying pills happens outside of react, make sure we're not doubly + // applying @room pills here, as a rerender with the same content won't touch the DOM + // to clear the pills from the last run of pillifyLinks + !node.parentElement.classList.contains("mx_AtRoomPill") + ) { + const Pill = sdk.getComponent('elements.Pill'); + + let currentTextNode = node; + const roomNotifTextNodes = []; + + // Take a textNode and break it up to make all the instances of @room their + // own textNode, adding those nodes to roomNotifTextNodes + while (currentTextNode !== null) { + const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); + let nextTextNode = null; + if (roomNotifPos > -1) { + let roomTextNode = currentTextNode; + + if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); + if (roomTextNode.textContent.length > Pill.roomNotifLen()) { + nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); + } + roomNotifTextNodes.push(roomTextNode); + } + currentTextNode = nextTextNode; + } + + if (roomNotifTextNodes.length > 0) { + const pushProcessor = new PushProcessor(MatrixClientPeg.get()); + const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); + if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) { + // Now replace all those nodes with Pills + for (const roomNotifTextNode of roomNotifTextNodes) { + // Set the next node to be processed to the one after the node + // we're adding now, since we've just inserted nodes into the structure + // we're iterating over. + // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once + node = roomNotifTextNode.nextSibling; + + const pillContainer = document.createElement('span'); + const pill = ; + + ReactDOM.render(pill, pillContainer); + roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); + } + // Nothing else to do for a text node (and we don't need to advance + // the loop pointer because we did it above) + continue; + } + } + } + + if (node.childNodes && node.childNodes.length && !pillified) { + pillifyLinks(node.childNodes, mxEvent); + } + + node = node.nextSibling; + } +} diff --git a/yarn.lock b/yarn.lock index a699e71efb..7b949781d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3694,10 +3694,10 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -highlight.js@9.14.2: - version "9.14.2" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.14.2.tgz#efbfb22dc701406e4da406056ef8c2b70ebe5b26" - integrity sha512-Nc6YNECYpxyJABGYJAyw7dBAYbXEuIzwzkqoJnwbc1nIpCiN+3ioYf0XrBnLiyyG0JLuJhpPtt2iTSbXiKLoyA== +highlight.js@^9.15.8: + version "9.15.8" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.8.tgz#f344fda123f36f1a65490e932cf90569e4999971" + integrity sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA== hmac-drbg@^1.0.0: version "1.0.1" @@ -4877,10 +4877,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.0.tgz#490b70e062ee24636536e3d9481e333733d00f2c" integrity sha512-3Zs9P/0zzwTob2pdgT0CHZuMbnSUSp8MB1bddfm+HDmnFWHGT4jvEZRf+2RuPoa+cjdn/z25SEt5gFTqdhvJAg== -matrix-js-sdk@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.0.0.tgz#1bbca0170c190a42eeda739f52f2995b446225ee" - integrity sha512-2/NV9hepV8QnX3xl1oaS7VS9tqsBKqCh9/pp+dAYFMA1A5PJACMFjE2M1KnW3JvHH+uDvag4fa7rraMwwGOK+A== +matrix-js-sdk@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.0.1.tgz#e9691c7fc142793aa8cd79e92d45698bcc5da8c4" + integrity sha512-+yj9fBdIE65v1+46TL/eLQGohtNZGBEtOD1n3nTAVBMogyVb2bpUWnqTli0ghiOCG9MKq7tWi+G4bDBTABxuxA== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0"