diff --git a/.babelrc b/.babelrc index 3fb847ad18..abe7e1ef3f 100644 --- a/.babelrc +++ b/.babelrc @@ -13,7 +13,6 @@ ], "transform-class-properties", "transform-object-rest-spread", - "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import" diff --git a/code_style.md b/code_style.md index e7844b939c..4b2338064c 100644 --- a/code_style.md +++ b/code_style.md @@ -22,7 +22,7 @@ number throgh from the original code to the final application. General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. -- 120 columns per line, but try to keep JavaScript code around the 80 column mark. +- 120 columns per line, but try to keep JavaScript code around the 90 column mark. Inline JSX in particular can be nicer with more columns per line. - No trailing whitespace at end of lines. - Don't indent empty lines. diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index e67c74a95c..00033b5b8c 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -2,8 +2,7 @@ The CIDER editor is a custom editor written for Riot. Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. -It is used to power the composer to edit messages, -and will soon be used as the main composer to send messages as well. +It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...). ## High-level overview. diff --git a/package.json b/package.json index eb234e0573..bafcbcbab0 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "dependencies": { "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", - "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -120,7 +119,6 @@ "babel-eslint": "^10.0.1", "babel-loader": "^7.1.5", "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-builtin-extend": "^1.1.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.26.0", @@ -135,6 +133,7 @@ "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", diff --git a/res/css/_common.scss b/res/css/_common.scss index 70ab2457f1..5987275f7f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -550,6 +550,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { color: $username-variant8-color; } +@define-mixin mx_Tooltip_dark { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + border: none; + border-radius: 3px; + padding: 6px 8px; +} + +// This is a workaround for our mixins not supporting child selectors +.mx_Tooltip_dark { + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} + @define-mixin mx_Settings_fullWidthField { margin-right: 100px; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..e39003fbec 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -48,6 +48,7 @@ @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; @import "./views/context_menus/_TopLeftMenu.scss"; +@import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @@ -172,7 +173,7 @@ @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; -@import "./views/settings/_IntegrationsManager.scss"; +@import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss new file mode 100644 index 0000000000..60b7b93f99 --- /dev/null +++ b/res/css/views/context_menus/_WidgetContextMenu.scss @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Matrix.org Foundaction 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_WidgetContextMenu { + padding: 6px; + + .mx_WidgetContextMenu_option { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; + } + + .mx_WidgetContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; + } +} diff --git a/res/css/views/dialogs/_TermsDialog.scss b/res/css/views/dialogs/_TermsDialog.scss index aad679a5b3..beb507e778 100644 --- a/res/css/views/dialogs/_TermsDialog.scss +++ b/res/css/views/dialogs/_TermsDialog.scss @@ -16,10 +16,10 @@ limitations under the License. /* * To avoid visual glitching of two modals stacking briefly, we customise the - * terms dialog sizing when it will appear for the integrations manager so that + * terms dialog sizing when it will appear for the integration manager so that * it gets the same basic size as the IM's own modal. */ -.mx_TermsDialog_forIntegrationsManager .mx_Dialog { +.mx_TermsDialog_forIntegrationManager .mx_Dialog { width: 60%; height: 70%; box-sizing: border-box; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index b4cde4e7ef..87a75dee82 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/normal.svg"); + mask-image: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,7 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { - mask: url("$(res)/img/e2e/verified.svg"); + mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c68f3ffd37..df7d0a5f87 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -195,6 +195,8 @@ limitations under the License. .mx_UserInfo_devices { .mx_UserInfo_device { display: flex; + margin: 8px 0; + &.mx_UserInfo_device_verified { .mx_UserInfo_device_trusted { @@ -210,6 +212,7 @@ limitations under the License. .mx_UserInfo_device_name { flex: 1; margin-right: 5px; + word-break: break-word; } } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 9ca6954af7..a3fe573ad0 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -153,40 +153,12 @@ $AppsDrawerBodyHeight: 273px; background-color: $accent-color; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_reload { - mask-image: url('$(res)/img/feather-customised/widget/refresh.svg'); - mask-size: 100%; -} - .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_snapshot { - mask-image: url('$(res)/img/feather-customised/widget/camera.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_edit { - mask-image: url('$(res)/img/feather-customised/widget/edit.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_delete { - mask-image: url('$(res)/img/feather-customised/widget/bin.svg'); - background-color: $warning-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_cancel { - mask-image: url('$(res)/img/feather-customised/widget/x-circle.svg'); -} - -/* delete ? */ -.mx_AppTileMenuBarWidget { - cursor: pointer; - width: 10px; - height: 10px; - padding: 1px; - transition-duration: 500ms; - border: 1px solid transparent; +.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { + mask-image: url('$(res)/img/icon_context.svg'); } .mx_AppTileMenuBarWidgetDelete { @@ -294,49 +266,61 @@ form.mx_Custom_Widget_Form div { .mx_AppPermissionWarning { text-align: center; - background-color: $primary-bg-color; + background-color: $widget-menu-bar-bg-color; display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; + font-size: 16px; } -.mx_AppPermissionWarningImage { - margin: 10px 0; +.mx_AppPermissionWarning_row { + margin-bottom: 12px; } -.mx_AppPermissionWarningImage img { - width: 100px; +.mx_AppPermissionWarning_smallText { + font-size: 12px; } -.mx_AppPermissionWarningText { - max-width: 90%; - margin: 10px auto 10px auto; - color: $primary-fg-color; +.mx_AppPermissionWarning_bolder { + font-weight: 600; } -.mx_AppPermissionWarningTextLabel { - font-weight: bold; - display: block; +.mx_AppPermissionWarning h4 { + margin: 0; + padding: 0; } -.mx_AppPermissionWarningTextURL { +.mx_AppPermissionWarning_helpIcon { + margin-top: 1px; + margin-right: 2px; + width: 10px; + height: 10px; display: inline-block; - max-width: 100%; - color: $accent-color; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } -.mx_AppPermissionButton { - border: none; - padding: 5px 20px; - border-radius: 5px; - background-color: $button-bg-color; - color: $button-fg-color; - cursor: pointer; +.mx_AppPermissionWarning_helpIcon::before { + display: inline-block; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: 12px; + width: 12px; + height: 12px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/feather-customised/help-circle.svg'); +} + +.mx_AppPermissionWarning_tooltip { + @mixin mx_Tooltip_dark; + + ul { + list-style-position: inside; + padding-left: 2px; + margin-left: 0; + } } .mx_AppLoading { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index bc11ac6e1c..1ee5008888 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -22,21 +22,6 @@ limitations under the License. display: block; } -.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { - content: ""; - display: block; - /* the symbols in the shield icons are cut out to make it themeable with css masking. - if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cut-out. - hardcoding white and not using a theme variable as this would probably be white for any theme. */ - background-color: white; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - .mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { content: ""; display: block; @@ -49,23 +34,11 @@ limitations under the License. mask-size: contain; } -.mx_E2EIcon_verified::before { - /* white rectangle below checkmark of shield */ - margin: 25% 28% 38% 25%; -} - - .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); background-color: $accent-color; } - -.mx_E2EIcon_warning::before { - /* white rectangle below "!" of shield */ - margin: 18% 40% 25% 40%; -} - .mx_E2EIcon_warning::after { mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 951d1945b1..e73e6c58f1 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -25,6 +25,7 @@ limitations under the License. width: 12px; height: 12px; mask-repeat: no-repeat; + mask-size: 100%; } .mx_MemberDeviceInfo_icon_blacklisted { mask-image: url('$(res)/img/e2e/blacklisted.svg'); diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 14562fe7ed..036756e2eb 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -23,10 +23,6 @@ limitations under the License. padding-left: 84px; } -.mx_MessageComposer_wrapper.mx_MessageComposer_hasE2EIcon { - padding-left: 109px; -} - .mx_MessageComposer_replaced_wrapper { margin-left: auto; margin-right: auto; @@ -78,10 +74,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - - &::after { - background-color: $composer-e2e-icon-color; - } + width: 16px; + height: 16px; + margin-right: 0; // Counteract the E2EIcon class + margin-left: 3px; // Counteract the E2EIcon class } .mx_MessageComposer_noperm_error { diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationManager.scss similarity index 83% rename from res/css/views/settings/_IntegrationsManager.scss rename to res/css/views/settings/_IntegrationManager.scss index 8b51eb272e..81b01ab8de 100644 --- a/res/css/views/settings/_IntegrationsManager.scss +++ b/res/css/views/settings/_IntegrationManager.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_IntegrationsManager .mx_Dialog { +.mx_IntegrationManager .mx_Dialog { width: 60%; height: 70%; overflow: hidden; @@ -23,22 +23,22 @@ limitations under the License. max-height: initial; } -.mx_IntegrationsManager iframe { +.mx_IntegrationManager iframe { background-color: #fff; border: 0px; width: 100%; height: 100%; } -.mx_IntegrationsManager_loading h3 { +.mx_IntegrationManager_loading h3 { text-align: center; } -.mx_IntegrationsManager_error { +.mx_IntegrationManager_error { text-align: center; padding-top: 20px; } -.mx_IntegrationsManager_error h3 { +.mx_IntegrationManager_error h3 { color: $warning-color; } diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 99537f9eb4..3e59ac73ac 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SetIntegrationManager .mx_Field_input { - @mixin mx_Settings_fullWidthField; -} - .mx_SetIntegrationManager { margin-top: 10px; margin-bottom: 10px; @@ -32,6 +28,10 @@ limitations under the License. padding-left: 5px; } -.mx_SetIntegrationManager_tooltip { - @mixin mx_Settings_tooltip; +.mx_SetIntegrationManager .mx_ToggleSwitch { + display: inline-block; + float: right; + top: 9px; + + @mixin mx_Settings_fullWidthField; } diff --git a/res/fonts/Nunito/Nunito-Bold.ttf b/res/fonts/Nunito/Nunito-Bold.ttf index c70de76bbd..c8fabf7d92 100644 Binary files a/res/fonts/Nunito/Nunito-Bold.ttf and b/res/fonts/Nunito/Nunito-Bold.ttf differ diff --git a/res/fonts/Nunito/Nunito-Regular.ttf b/res/fonts/Nunito/Nunito-Regular.ttf index 064e805431..86ce522f60 100644 Binary files a/res/fonts/Nunito/Nunito-Regular.ttf and b/res/fonts/Nunito/Nunito-Regular.ttf differ diff --git a/res/fonts/Nunito/Nunito-SemiBold.ttf b/res/fonts/Nunito/Nunito-SemiBold.ttf index a84b3b35a6..8bf953b59a 100644 Binary files a/res/fonts/Nunito/Nunito-SemiBold.ttf and b/res/fonts/Nunito/Nunito-SemiBold.ttf differ diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf deleted file mode 100644 index 4387fb67c4..0000000000 Binary files a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf and /dev/null differ diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf deleted file mode 100644 index 68fb3ff5cb..0000000000 Binary files a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf and /dev/null differ diff --git a/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf b/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf deleted file mode 100644 index c40e599260..0000000000 Binary files a/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf and /dev/null differ diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf deleted file mode 100644 index 0c4fd17dfa..0000000000 Binary files a/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf and /dev/null differ diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf deleted file mode 100644 index 339d59ac00..0000000000 Binary files a/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf and /dev/null differ diff --git a/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf b/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf deleted file mode 100644 index b5fcd891af..0000000000 Binary files a/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf and /dev/null differ diff --git a/res/img/feather-customised/widget/bin.svg b/res/img/feather-customised/widget/bin.svg deleted file mode 100644 index 7616d8931b..0000000000 --- a/res/img/feather-customised/widget/bin.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/res/img/feather-customised/widget/camera.svg b/res/img/feather-customised/widget/camera.svg deleted file mode 100644 index 5502493068..0000000000 --- a/res/img/feather-customised/widget/camera.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/edit.svg b/res/img/feather-customised/widget/edit.svg deleted file mode 100644 index 749e83f982..0000000000 --- a/res/img/feather-customised/widget/edit.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/refresh.svg b/res/img/feather-customised/widget/refresh.svg deleted file mode 100644 index 0994bbdd52..0000000000 --- a/res/img/feather-customised/widget/refresh.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/x-circle.svg b/res/img/feather-customised/widget/x-circle.svg deleted file mode 100644 index 951407b39c..0000000000 --- a/res/img/feather-customised/widget/x-circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/BasePlatform.js b/src/BasePlatform.js index a97c14bf90..14e34a1f40 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -19,6 +19,7 @@ limitations under the License. */ import dis from './dispatcher'; +import BaseEventIndexManager from './indexing/BaseEventIndexManager'; /** * Base class for classes that provide platform-specific functionality @@ -151,4 +152,14 @@ export default class BasePlatform { async setMinimizeToTrayEnabled(enabled: boolean): void { throw new Error("Unimplemented"); } + + /** + * Get our platform specific EventIndexManager. + * + * @return {BaseEventIndexManager} The EventIndex manager for our platform, + * can be null if the platform doesn't support event indexing. + */ + getEventIndexingManager(): BaseEventIndexManager | null { + return null; + } } diff --git a/src/CallHandler.js b/src/CallHandler.js index bcdf7853fd..9350fe4dd9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -382,7 +382,7 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integrations manager. Technically we could put + // check for a working integration 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. @@ -495,6 +495,17 @@ async function _startCallApp(roomId, type) { // with the dispatcher once if (!global.mxCallHandler) { dis.register(_onAction); + // add empty handlers for media actions, otherwise the media keys + // end up causing the audio elements with our ring/ringback etc + // audio clips in to play. + if (navigator.mediaSession) { + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); + } } const callHandler = { diff --git a/src/ContentMessages.js b/src/ContentMessages.js index dab8de2465..6908a6a18e 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; -import Promise from 'bluebird'; import extend from './extend'; import dis from './dispatcher'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ffd5baace4..b81b563129 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -16,10 +16,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; @@ -525,7 +525,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).done(); + ); } export function softLogout() { @@ -589,6 +589,7 @@ async function startMatrixClient(startSyncing=true) { if (startSyncing) { await MatrixClientPeg.start(); + await EventIndexPeg.init(); } else { console.warn("Caller requested only auxiliary services be started"); await MatrixClientPeg.assign(); @@ -607,20 +608,20 @@ async function startMatrixClient(startSyncing=true) { * Stops a running client and all related services, and clears persistent * storage. Used after a session has been logged out. */ -export function onLoggedOut() { +export async function onLoggedOut() { _isLoggingOut = false; // Ensure that we dispatch a view change **before** stopping the client so // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().done(); + await _clearStorage(); } /** * @returns {Promise} promise which resolves once the stores have been cleared */ -function _clearStorage() { +async function _clearStorage() { Analytics.logout(); if (window.localStorage) { @@ -632,7 +633,9 @@ function _clearStorage() { // we'll never make any requests, so can pass a bogus HS URL baseUrl: "", }); - return cli.clearStores(); + + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); } /** @@ -649,6 +652,7 @@ export function stopMatrixClient(unsetClient=true) { IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); + EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); if (cli) { cli.stopClient(); @@ -656,6 +660,7 @@ export function stopMatrixClient(unsetClient=true) { if (unsetClient) { MatrixClientPeg.unset(); + EventIndexPeg.unset(); } } } diff --git a/src/Modal.js b/src/Modal.js index cb19731f01..4fc9fdcb02 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,7 +23,6 @@ import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; -import Promise from "bluebird"; import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; diff --git a/src/Notifier.js b/src/Notifier.js index cca0ea2b89..edb9850dfe 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().done((result) => { + plaf.requestNotificationPermission().then((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 4eaee16d1b..51ec804c01 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 2d5e4b3136..5bef4afd25 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -17,7 +17,6 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; -import Promise from 'bluebird'; export const ALL_MESSAGES_LOUD = 'all_messages_loud'; export const ALL_MESSAGES = 'all_messages'; diff --git a/src/Rooms.js b/src/Rooms.js index c8f90ec39a..239e348b58 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -15,7 +15,6 @@ limitations under the License. */ import MatrixClientPeg from './MatrixClientPeg'; -import Promise from 'bluebird'; /** * Given a room object, return the alias we should use for it, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 3623d47f8e..92f0ff6340 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -16,7 +16,6 @@ limitations under the License. */ import url from 'url'; -import Promise from 'bluebird'; import SettingsStore from "./settings/SettingsStore"; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; const request = require('browser-request'); diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 910a6c4f13..c0ffc3022d 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).done(function() { + client.invite(roomId, userId).then(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { sendResponse(event, { success: true, }); diff --git a/src/Searching.js b/src/Searching.js new file mode 100644 index 0000000000..f8976c92e4 --- /dev/null +++ b/src/Searching.js @@ -0,0 +1,138 @@ +/* +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 EventIndexPeg from "./indexing/EventIndexPeg"; +import MatrixClientPeg from "./MatrixClientPeg"; + +function serverSideSearch(term, roomId = undefined) { + let filter; + if (roomId !== undefined) { + // XXX: it's unintuitive that the filter for searching doesn't have + // the same shape as the v2 filter API :( + filter = { + rooms: [roomId], + }; + } + + const searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter, + term, + }); + + return searchPromise; +} + +async function combinedSearch(searchTerm) { + // Create two promises, one for the local search, one for the + // server-side search. + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearch(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separately, when we combine them the order might not + // be the right one so we need to sort them. + const compare = (a, b) => { + const aEvent = a.context.getEvent().event; + const bEvent = b.context.getEvent().event; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; +} + +async function localSearch(searchTerm, roomId = undefined) { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + room_id: undefined, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const eventIndex = EventIndexPeg.get(); + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; +} + +function eventIndexSearch(term, roomId = undefined) { + let searchPromise; + + if (roomId !== undefined) { + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearch(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = serverSideSearch(term, roomId); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearch(term); + } + + return searchPromise; +} + +export default function eventSearch(term, roomId = undefined) { + const eventIndex = EventIndexPeg.get(); + + if (eventIndex === null) return serverSideSearch(term, roomId); + else return eventIndexSearch(term, roomId); +} diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1a491da54f..31e7ca4f39 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -28,7 +28,6 @@ import { linkifyAndSanitizeHtml } from './HtmlUtils'; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import WidgetUtils from "./utils/WidgetUtils"; import {textToHtmlRainbow} from "./utils/colour"; -import Promise from "bluebird"; import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; diff --git a/src/Terms.js b/src/Terms.js index 685a39709c..14a7ccb65e 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js index def4af56ae..00309d252c 100644 --- a/src/ToWidgetPostMessageApi.js +++ b/src/ToWidgetPostMessageApi.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - // const OUTBOUND_API_NAME = 'toWidget'; // Initiate requests using the "toWidget" postMessage API and handle responses diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index 37b3a7ddad..e0e333a371 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import {createNewMatrixCall, Room} from "matrix-js-sdk"; import CallHandler from './CallHandler'; import MatrixClientPeg from "./MatrixClientPeg"; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index c385e13878..a26eb6033b 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -26,7 +26,6 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import Promise from 'bluebird'; import {timeout} from "../utils/promise"; export type SelectionRange = { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 776e7f0d6d..a0aa36803f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -637,7 +636,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).done(); + }); }, _onJoinableChange: function(ev) { @@ -676,7 +675,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).done(); + }); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 5e06d124c4..e1b02f653b 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).done(); + }); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 620e73bf93..9e1cd2f1dd 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -17,8 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -59,14 +57,10 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; -import { setTheme } from "../../theme"; +import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; -// Disable warnings for now: we use deprecated bluebird functions -// and need to migrate, but they spam the console with warnings. -Promise.config({warnings: false}); - /** constants for MatrixChat.state.view */ const VIEWS = { // a special initial state which is only used at startup, while we are @@ -274,6 +268,8 @@ export default createReactClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._themeWatcher = new ThemeWatcher(); + this._themeWatcher.start(); this.focusComposer = false; @@ -360,6 +356,7 @@ export default createReactClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); + this._themeWatcher.stop(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); @@ -542,7 +539,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).done(() => { + MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -663,9 +660,6 @@ export default createReactClass({ }); break; } - case 'set_theme': - setTheme(payload.value); - break; case 'on_logging_in': // We are now logging in, so set the state to reflect that // NB. This does not touch 'ready' since if our dispatches @@ -863,7 +857,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.done(() => { + waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -980,7 +974,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).done(); + createRoom({createOpts}); } }, @@ -1376,17 +1370,6 @@ export default createReactClass({ }, null, true); }); - cli.on("accountData", function(ev) { - if (ev.getType() === 'im.vector.web.settings') { - if (ev.getContent() && ev.getContent().theme) { - dis.dispatch({ - action: 'set_theme', - value: ev.getContent().theme, - }); - } - } - }); - const dft = new DecryptionFailureTracker((total, errorCode) => { Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); }, (errorCode) => { @@ -1756,7 +1739,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2de15a5444..63ae14ba09 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 48d272f6c9..895f6ae57e 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -185,7 +185,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { panel = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", @@ -204,7 +204,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 84f402e484..efca8d12a8 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ const dis = require('../../dispatcher'); import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { _t } from '../../languageHandler'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; @@ -89,7 +88,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +134,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().done(); + this.getMoreRooms(); }, getMoreRooms: function() { @@ -246,7 +245,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).done(() => { + }).then(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +347,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 21dd06767c..b0aa4cb59b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -289,7 +289,7 @@ module.exports = createReactClass({ } return
- +
{ title } @@ -306,7 +306,7 @@ module.exports = createReactClass({ if (this._shouldShowConnectionError()) { return (
- /!\ + /!\
{ _t('Connectivity to the server has been lost.') } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..0bf61d2e0c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -27,7 +27,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import classNames from 'classnames'; import {Room} from "matrix-js-sdk"; import { _t } from '../../languageHandler'; @@ -43,6 +42,7 @@ import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; +import eventSearch from '../../Searching'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; @@ -1101,7 +1101,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .done(undefined, (error) => { + .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1129,23 +1129,12 @@ module.exports = createReactClass({ // todo: should cancel any previous search requests. this.searchId = new Date().getTime(); - let filter; - if (scope === "Room") { - filter = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( - rooms: [ - this.state.room.roomId, - ], - }; - } + let roomId; + if (scope === "Room") roomId = this.state.room.roomId; debuglog("sending search request"); - - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - this._handleSearchResult(searchPromise).done(); + const searchPromise = eventSearch(term, roomId); + this._handleSearchResult(searchPromise); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1305,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1322,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 1d5c520285..8a67e70467 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3dd5ea761e..80cbd43079 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -23,7 +23,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; const Matrix = require("matrix-js-sdk"); const EventTimeline = Matrix.EventTimeline; @@ -462,7 +461,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); @@ -1076,6 +1075,7 @@ const TimelinePanel = createReactClass({ if (timeline) { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline + this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time onLoaded(); } else { const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46a5fa7bd7..6f68293caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).done(() => { + this.reset.resetPassword(email, password).then(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 0dd84d100d..b2e9d3e7cd 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, onUsernameChanged: function(username) { @@ -439,7 +439,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 66075c80f7..760163585d 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { + cli.getProfileInfo(cli.credentials.userId).then(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6321028457..3578d745f5 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -18,7 +18,6 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -371,7 +370,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).then(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d19ce95b33..cc3f9f96c4 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).done(); + }); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index fb056ee47f..97433e1f77 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -160,7 +160,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -190,7 +190,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js new file mode 100644 index 0000000000..43e7e172cc --- /dev/null +++ b/src/components/views/context_menus/WidgetContextMenu.js @@ -0,0 +1,134 @@ +/* +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 sdk from '../../../index'; +import {_t} from '../../../languageHandler'; + +export default class WidgetContextMenu extends React.Component { + static propTypes = { + onFinished: PropTypes.func, + + // Callback for when the revoke button is clicked. Required. + onRevokeClicked: PropTypes.func.isRequired, + + // Callback for when the snapshot button is clicked. Button not shown + // without a callback. + onSnapshotClicked: PropTypes.func, + + // Callback for when the reload button is clicked. Button not shown + // without a callback. + onReloadClicked: PropTypes.func, + + // Callback for when the edit button is clicked. Button not shown + // without a callback. + onEditClicked: PropTypes.func, + + // Callback for when the delete button is clicked. Button not shown + // without a callback. + onDeleteClicked: PropTypes.func, + }; + + proxyClick(fn) { + fn(); + if (this.props.onFinished) this.props.onFinished(); + } + + // XXX: It's annoying that our context menus require us to hit onFinished() to close :( + + onEditClicked = () => { + this.proxyClick(this.props.onEditClicked); + }; + + onReloadClicked = () => { + this.proxyClick(this.props.onReloadClicked); + }; + + onSnapshotClicked = () => { + this.proxyClick(this.props.onSnapshotClicked); + }; + + onDeleteClicked = () => { + this.proxyClick(this.props.onDeleteClicked); + }; + + onRevokeClicked = () => { + this.proxyClick(this.props.onRevokeClicked); + }; + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + const options = []; + + if (this.props.onEditClicked) { + options.push( + + {_t("Edit")} + , + ); + } + + if (this.props.onReloadClicked) { + options.push( + + {_t("Reload")} + , + ); + } + + if (this.props.onSnapshotClicked) { + options.push( + + {_t("Take picture")} + , + ); + } + + if (this.props.onDeleteClicked) { + options.push( + + {_t("Remove for everyone")} + , + ); + } + + // Push this last so it appears last. It's always present. + options.push( + + {_t("Remove for me")} + , + ); + + // Put separators between the options + if (options.length > 1) { + const length = options.length; + for (let i = 0; i < length - 1; i++) { + const sep =
; + + // Insert backwards so the insertions don't affect our math on where to place them. + // We also use our cached length to avoid worrying about options.length changing + options.splice(length - 1 - i, 0, sep); + } + } + + return
{options}
; + } +} diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 24d8b96e0c..a40495893d 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -266,7 +266,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).done(() => { + }).then(() => { this.setState({ busy: false, }); @@ -379,7 +379,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).done(() => { + }).then(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 11f4c21366..3430a12e71 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).done(); + }); }, _onCancel: function() { diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js new file mode 100644 index 0000000000..3ab1123f8b --- /dev/null +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js @@ -0,0 +1,57 @@ +/* +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 dis from '../../../dispatcher'; + +export default class IntegrationsDisabledDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + _onOpenSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({action: "view_user_settings"}); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
+

{_t("Enable 'Manage Integrations' in Settings to do this.")}

+
+ +
+ ); + } +} diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js new file mode 100644 index 0000000000..9927f627f1 --- /dev/null +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -0,0 +1,55 @@ +/* +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"; + +export default class IntegrationsImpossibleDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
+

+ {_t( + "Your Riot doesn't allow you to use an Integration Manager to do this. " + + "Please contact an admin.", + )} +

+
+ +
+ ); + } +} diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index a10c25a0fb..01e3479bb1 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).done(); + }); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index bedf713c4e..b527abffc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).done(() => { + this._addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 3bc6f5597e..598d0ce354 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js index 5ef7aef9ab..e86a46fb36 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -82,10 +82,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component { client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -139,7 +139,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { } _renderTab() { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); let uiUrl = null; if (this.state.currentScalarClient) { uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( @@ -148,7 +148,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { this.props.integrationId, ); } - return {_t("Identity Server")}
({host})
; case Matrix.SERVICE_TYPES.IM: - return
{_t("Integrations Manager")}
({host})
; + return
{_t("Integration Manager")}
({host})
; } } diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 1e019c0287..8dc58643bd 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -19,79 +19,126 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import url from 'url'; +import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import WidgetUtils from "../../../utils/WidgetUtils"; +import MatrixClientPeg from "../../../MatrixClientPeg"; export default class AppPermission extends React.Component { + static propTypes = { + url: PropTypes.string.isRequired, + creatorUserId: PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, + onPermissionGranted: PropTypes.func.isRequired, + isRoomEncrypted: PropTypes.bool, + }; + + static defaultProps = { + onPermissionGranted: () => {}, + }; + constructor(props) { super(props); - const curlBase = this.getCurlBase(); - this.state = { curlBase: curlBase}; + // The first step is to pick apart the widget so we can render information about it + const urlInfo = this.parseWidgetUrl(); + + // The second step is to find the user's profile so we can show it on the prompt + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + let roomMember; + if (room) roomMember = room.getMember(this.props.creatorUserId); + + // Set all this into the initial state + this.state = { + ...urlInfo, + roomMember, + }; } - // Return string representation of content URL without query parameters - getCurlBase() { - const wurl = url.parse(this.props.url); - let curl; - let curlString; + parseWidgetUrl() { + const widgetUrl = url.parse(this.props.url); + const params = new URLSearchParams(widgetUrl.search); - const searchParams = new URLSearchParams(wurl.search); - - if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) { - curl = url.parse(searchParams.get('url')); - if (curl) { - curl.search = curl.query = ""; - curlString = curl.format(); - } + // HACK: We're relying on the query params when we should be relying on the widget's `data`. + // This is a workaround for Scalar. + if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) { + const unwrappedUrl = url.parse(params.get('url')); + return { + widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, + isWrapped: true, + }; + } else { + return { + widgetDomain: widgetUrl.host || widgetUrl.hostname, + isWrapped: false, + }; } - if (!curl && wurl) { - wurl.search = wurl.query = ""; - curlString = wurl.format(); - } - return curlString; } render() { - let e2eWarningText; - if (this.props.isRoomEncrypted) { - e2eWarningText = - { _t('NOTE: Apps are not end-to-end encrypted') }; - } - const cookieWarning = - - { _t('Warning: This widget might use cookies.') } - ; + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip"); + + const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId; + const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId; + + const avatar = this.state.roomMember + ? + : ; + + const warningTooltipText = ( +
+ {_t("Any of the following data may be shared:")} +
    +
  • {_t("Your display name")}
  • +
  • {_t("Your avatar URL")}
  • +
  • {_t("Your user ID")}
  • +
  • {_t("Your theme")}
  • +
  • {_t("Riot URL")}
  • +
  • {_t("Room ID")}
  • +
  • {_t("Widget ID")}
  • +
+
+ ); + const warningTooltip = ( + + + + ); + + // Due to i18n limitations, we can't dedupe the code for variables in these two messages. + const warning = this.state.isWrapped + ? _t("Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}) + : _t("Using this widget may share data with %(widgetDomain)s.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); + + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null; + return (
-
- {_t('Warning!')} +
+ {_t("Widget added by")}
-
- {_t('Do you want to load widget from URL:')} - {this.state.curlBase} - { e2eWarningText } - { cookieWarning } +
+ {avatar} +

{displayName}

+
{userId}
+
+
+ {warning} +
+
+ {_t("This widget may use cookies.")} {encryptionWarning} +
+
+ + {_t("Continue")} +
-
); } } - -AppPermission.propTypes = { - isRoomEncrypted: PropTypes.bool, - url: PropTypes.string.isRequired, - onPermissionGranted: PropTypes.func.isRequired, -}; -AppPermission.defaultProps = { - isRoomEncrypted: false, - onPermissionGranted: function() {}, -}; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..746631a99e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -34,7 +34,8 @@ import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import {createMenu} from "../../structures/ContextualMenu"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -52,7 +53,7 @@ export default class AppTile extends React.Component { this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); - this._onCancelClick = this._onCancelClick.bind(this); + this._onRevokeClicked = this._onRevokeClicked.bind(this); this._onSnapshotClick = this._onSnapshotClick.bind(this); this.onClickMenuBar = this.onClickMenuBar.bind(this); this._onMinimiseClick = this._onMinimiseClick.bind(this); @@ -69,8 +70,11 @@ export default class AppTile extends React.Component { * @return {Object} Updated component state to be set with setState */ _getNewState(newProps) { - const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_'); - const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); + // This is a function to make the impact of calling SettingsStore slightly less + const hasPermissionToLoad = () => { + const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); + return !!currentlyAllowedWidgets[newProps.eventId]; + }; const PersistedElement = sdk.getComponent("elements.PersistedElement"); return { @@ -78,10 +82,9 @@ export default class AppTile extends React.Component { // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), widgetUrl: this._addWurlParams(newProps.url), - widgetPermissionId: widgetPermissionId, // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user - hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, + hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), error: null, deleting: false, widgetPageTitle: newProps.widgetPageTitle, @@ -205,7 +208,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().done((token) => { + this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); @@ -269,7 +272,7 @@ export default class AppTile extends React.Component { return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); } - _onEditClick(e) { + _onEditClick() { console.log("Edit widget ID ", this.props.id); if (this.props.onEditClick) { this.props.onEditClick(); @@ -291,7 +294,7 @@ export default class AppTile extends React.Component { } } - _onSnapshotClick(e) { + _onSnapshotClick() { console.warn("Requesting widget snapshot"); ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot() .catch((err) => { @@ -358,13 +361,9 @@ export default class AppTile extends React.Component { } } - _onCancelClick() { - if (this.props.onDeleteClick) { - this.props.onDeleteClick(); - } else { - console.log("Revoke widget permissions - %s", this.props.id); - this._revokeWidgetPermission(); - } + _onRevokeClicked() { + console.log("Revoke widget permissions - %s", this.props.id); + this._revokeWidgetPermission(); } /** @@ -446,24 +445,38 @@ export default class AppTile extends React.Component { }); } - /* TODO -- Store permission in account data so that it is persisted across multiple devices */ _grantWidgetPermission() { - console.warn('Granting permission to load widget - ', this.state.widgetUrl); - localStorage.setItem(this.state.widgetPermissionId, true); - this.setState({hasPermissionToLoad: true}); - // Now that we have permission, fetch the IM token - this.setScalarToken(); + const roomId = this.props.room.roomId; + console.info("Granting permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = true; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: true}); + + // Fetch a token for the integration manager, now that we're allowed to + this.setScalarToken(); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } _revokeWidgetPermission() { - console.warn('Revoking permission to load widget - ', this.state.widgetUrl); - localStorage.removeItem(this.state.widgetPermissionId); - this.setState({hasPermissionToLoad: false}); + const roomId = this.props.room.roomId; + console.info("Revoking permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = false; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: false}); - // Force the widget to be non-persistent - ActiveWidgetStore.destroyPersistentWidget(this.props.id); - const PersistedElement = sdk.getComponent("elements.PersistedElement"); - PersistedElement.destroyElement(this._persistKey); + // Force the widget to be non-persistent (able to be deleted/forgotten) + ActiveWidgetStore.destroyPersistentWidget(this.props.id); + const PersistedElement = sdk.getComponent("elements.PersistedElement"); + PersistedElement.destroyElement(this._persistKey); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } formatAppTileName() { @@ -528,18 +541,59 @@ export default class AppTile extends React.Component { } } - _onPopoutWidgetClick(e) { + _onPopoutWidgetClick() { // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getSafeUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), { target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click(); } - _onReloadWidgetClick(e) { + _onReloadWidgetClick() { // Reload iframe in this way to avoid cross-origin restrictions this.refs.appFrame.src = this.refs.appFrame.src; } + _getMenuOptions(ev) { + // TODO: This block of code gets copy/pasted a lot. We should make that happen less. + const menuOptions = {}; + const buttonRect = ev.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonLeft = buttonRect.left + window.pageXOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the left edge of the button + menuOptions.right = window.innerWidth - buttonLeft; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonTop < window.innerHeight / 2) { + menuOptions.top = buttonTop; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + return menuOptions; + } + + _onContextMenuClick = (ev) => { + const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); + const menuOptions = { + ...this._getMenuOptions(ev), + + // A revoke handler is always required + onRevokeClicked: this._onRevokeClicked, + }; + + const canUserModify = this._canUserModify(); + const showEditButton = Boolean(this._scalarClient && canUserModify); + const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; + const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; + + if (showEditButton) menuOptions.onEditClicked = this._onEditClick; + if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick; + if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick; + if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick; + + createMenu(WidgetContextMenu, menuOptions); + }; + render() { let appTileBody; @@ -549,7 +603,7 @@ export default class AppTile extends React.Component { } // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin - // because that would allow the iframe to prgramatically remove the sandbox attribute, but + // because that would allow the iframe to programmatically remove the sandbox attribute, but // this would only be for content hosted on the same origin as the riot client: anything // hosted on the same origin as the client will get the same access as if you clicked // a link to it. @@ -569,12 +623,14 @@ export default class AppTile extends React.Component {
); if (!this.state.hasPermissionToLoad) { - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
@@ -627,13 +683,6 @@ export default class AppTile extends React.Component { } } - // editing is done in scalar - const canUserModify = this._canUserModify(); - const showEditButton = Boolean(this._scalarClient && canUserModify); - const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; - const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton; - // Picture snapshot - only show button when apps are maximised. - const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; const showMinimiseButton = this.props.showMinimise && this.props.show; const showMaximiseButton = this.props.showMinimise && !this.props.show; @@ -672,41 +721,17 @@ export default class AppTile extends React.Component { { this.props.showTitle && this._getTileTitle() } - { /* Reload widget */ } - { this.props.showReload && } { /* Popout widget */ } { this.props.showPopout && } - { /* Snapshot widget */ } - { showPictureSnapshotButton && } - { /* Edit widget */ } - { showEditButton && } - { /* Delete widget */ } - { showDeleteButton && } - { /* Cancel widget */ } - { showCancelButton && }
} @@ -720,6 +745,7 @@ AppTile.displayName ='AppTile'; AppTile.propTypes = { id: PropTypes.string.isRequired, + eventId: PropTypes.string, // required for room widgets url: PropTypes.string.isRequired, name: PropTypes.string.isRequired, room: PropTypes.object.isRequired, diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 3bf37df951..5cba98470c 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import Promise from 'bluebird'; /** * A component which wraps an EditableText, with a spinner while updates take @@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().done( + this.props.getInitialValue().then( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).done( + this.props.onSubmit(value).then( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e53e1ec0fa..e36464c4ef 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2772363bd0..b2f6d0abbb 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).done(); + }); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 365f9ded61..451c97d958 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).done(); + }); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index d6931850be..19e4be6083 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,13 +67,15 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId, + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), + persistentWidgetInRoomId, appEvent.getId(), ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); return ); diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 7d80bdd209..3cd5731b99 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index b4f26d0cbd..0246d28542 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).done((url) => { + }).then((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 640baa1966..b12957a7df 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -24,7 +24,6 @@ import MFileBody from './MFileBody'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -289,7 +288,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).done(); + }); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d277b6eae9..43e4f2dd75 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -20,7 +20,6 @@ import createReactClass from 'create-react-class'; import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -115,7 +114,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).done(); + }); } }, diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index b70724d516..d7e1ef3488 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -43,7 +43,8 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { if (room) { const senders = []; for (const reactionEvent of reactionEvents) { - const { name } = room.getMember(reactionEvent.getSender()); + const member = room.getMember(reactionEvent.getSender()); + const name = member ? member.name : reactionEvent.getSender(); senders.push(name); } const shortName = unicodeToShortcode(content); diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index aab6c04f53..952c49828b 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 2a0a7569fb..e53570dc5b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,7 +107,9 @@ module.exports = createReactClass({ this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room), ); return widgets.map((ev) => { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender); + return WidgetUtils.makeAppConfig( + ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(), + ); }); }, @@ -159,6 +161,7 @@ module.exports = createReactClass({ return ({ console.error("Failed to get URL preview: " + error); - }).done(); + }); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 9364f2f49d..1872ab112a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).done(function(devices) { + }).then(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -581,7 +581,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onPowerChange: async function(powerLevel) { @@ -638,7 +638,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onLeaveClick: function() { diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 632ca53f82..128f9be964 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -25,7 +25,6 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; -import classNames from 'classnames'; import E2EIcon from './E2EIcon'; function ComposerAvatar(props) { @@ -353,13 +352,9 @@ export default class MessageComposer extends React.Component { ); } - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
-
+
{ controls }
diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js index 4bb2f29e61..eb41f6729b 100644 --- a/src/components/views/rooms/SlateMessageComposer.js +++ b/src/components/views/rooms/SlateMessageComposer.js @@ -460,13 +460,9 @@ export default class SlateMessageComposer extends React.Component { const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
-
+
{ controls }
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 28e51ed12e..7eabf27528 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -74,10 +74,10 @@ export default class Stickerpicker extends React.Component { this.forceUpdate(); return this.scalarClient; }).catch((e) => { - this._imError(_td("Failed to connect to integrations server"), e); + this._imError(_td("Failed to connect to integration manager"), e); }); } else { - this._imError(_td("No integrations server is configured to manage stickers with")); + IntegrationManagers.sharedInstance().openNoManagerDialog(); } } @@ -287,12 +287,17 @@ export default class Stickerpicker extends React.Component { return stickersContent; } - /** + // Dev note: this isn't jsdoc because it's angry. + /* * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. - * @param {Event} e Event that triggered the function */ _onShowStickersClick(e) { + if (!SettingsStore.getValue("integrationProvisioning")) { + // Intercept this case and spawn a warning. + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button const buttonRect = e.target.getBoundingClientRect(); @@ -346,7 +351,7 @@ export default class Stickerpicker extends React.Component { } /** - * Launch the integrations manager on the stickers integration page + * Launch the integration manager on the stickers integration page */ _launchManageIntegrations() { // TODO: Open the right integration manager for the widget diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 32521006c7..904b17b15f 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.done(function() { + httpPromise.then(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..a317c46cec 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -25,7 +25,6 @@ const Modal = require("../../../Modal"); const sdk = require("../../../index"); import dis from "../../../dispatcher"; -import Promise from 'bluebird'; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; @@ -174,7 +173,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).done(); + }); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 30f507ea18..cb5db10be4 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().done( + MatrixClientPeg.get().getDevices().then( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationManager.js similarity index 71% rename from src/components/views/settings/IntegrationsManager.js rename to src/components/views/settings/IntegrationManager.js index d463b043d5..1ab17ca8a0 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -21,12 +21,9 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; -export default class IntegrationsManager extends React.Component { +export default class IntegrationManager extends React.Component { static propTypes = { - // false to display an error saying that there is no integrations manager configured - configured: PropTypes.bool.isRequired, - - // false to display an error saying that we couldn't connect to the integrations manager + // false to display an error saying that we couldn't connect to the integration manager connected: PropTypes.bool.isRequired, // true to display a loading spinner @@ -40,7 +37,6 @@ export default class IntegrationsManager extends React.Component { }; static defaultProps = { - configured: true, connected: true, loading: false, }; @@ -70,20 +66,11 @@ export default class IntegrationsManager extends React.Component { }; render() { - if (!this.props.configured) { - return ( -
-

{_t("No integrations server configured")}

-

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

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

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

+
+

{_t("Connecting to integration manager...")}

); @@ -91,9 +78,9 @@ export default class IntegrationsManager extends React.Component { if (!this.props.connected) { return ( -
-

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

-

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

+
+

{_t("Cannot connect to integration manager")}

+

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

); } diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e3b4cfe122..6c71101eb8 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import Promise from 'bluebird'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -97,7 +96,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { self._refreshFromServer(); }); }, @@ -170,7 +169,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.done(() => { + emailPusherPromise.then(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +273,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function() { + Promise.all(deferreds).then(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +342,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +397,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).done(function(resps) { + Promise.all(removeDeferreds).then(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +433,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +649,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).done(); + }); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index b1268c8048..e205f02e6c 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -16,13 +16,9 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; -import sdk from '../../../index'; -import Field from "../elements/Field"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import MatrixClientPeg from "../../../MatrixClientPeg"; -import {SERVICE_TYPES} from "matrix-js-sdk"; -import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance"; -import Modal from "../../../Modal"; +import sdk from '../../../index'; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -32,135 +28,23 @@ export default class SetIntegrationManager extends React.Component { this.state = { currentManager, - url: "", // user-entered text - error: null, - busy: false, - checking: false, + provisioningEnabled: SettingsStore.getValue("integrationProvisioning"), }; } - _onUrlChanged = (ev) => { - const u = ev.target.value; - this.setState({url: u}); - }; + onProvisioningToggled = () => { + const current = this.state.provisioningEnabled; + SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => { + console.error("Error changing integration manager provisioning"); + console.error(err); - _getTooltip = () => { - if (this.state.checking) { - const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); - return
- - { _t("Checking server") } -
; - } else if (this.state.error) { - return {this.state.error}; - } else { - return null; - } - }; - - _canChange = () => { - return !!this.state.url && !this.state.busy; - }; - - _continueTerms = async (manager) => { - try { - await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); - this.setState({ - busy: false, - error: null, - currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(), - url: "", // clear input - }); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Failed to update integration manager"), - }); - } - }; - - _setManager = async (ev) => { - // Don't reload the page when the user hits enter in the form. - ev.preventDefault(); - ev.stopPropagation(); - - this.setState({busy: true, checking: true, error: null}); - - let offline = false; - let manager: IntegrationManagerInstance; - try { - manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); - offline = !manager; // no manager implies offline - } catch (e) { - console.error(e); - offline = true; // probably a connection error - } - if (offline) { - this.setState({ - busy: false, - checking: false, - error: _t("Integration manager offline or not accessible."), - }); - return; - } - - // Test the manager (causes terms of service prompt if agreement is needed) - // We also cancel the tooltip at this point so it doesn't collide with the dialog. - this.setState({checking: false}); - try { - const client = manager.getScalarClient(); - await client.connect(); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Terms of service not accepted or the integration manager is invalid."), - }); - return; - } - - // Specifically request the terms of service to see if there are any. - // The above won't trigger a terms of service check if there are no terms to - // sign, so when there's no terms at all we need to ensure we tell the user. - let hasTerms = true; - try { - const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl); - hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0; - } catch (e) { - // Assume errors mean there are no terms. This could be a 404, 500, etc - console.error(e); - hasTerms = false; - } - if (!hasTerms) { - this.setState({busy: false}); - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Integration manager has no terms of service"), - description: ( -
- - {_t("The integration manager you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._continueTerms(manager); - }, - }); - return; - } - - this._continueTerms(manager); + this.setState({provisioningEnabled: current}); + }); + this.setState({provisioningEnabled: !current}); }; render() { - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch"); const currentManager = this.state.currentManager; let managerName; @@ -168,45 +52,32 @@ export default class SetIntegrationManager extends React.Component { if (currentManager) { managerName = `(${currentManager.name})`; bodyText = _t( - "You are currently using %(serverName)s to manage your bots, widgets, " + + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, " + "and sticker packs.", {serverName: currentManager.name}, { b: sub => {sub} }, ); } else { - bodyText = _t( - "Add which integration manager you want to manage your bots, widgets, " + - "and sticker packs.", - ); + bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs."); } return ( -
+
- {_t("Integration Manager")} + {_t("Manage integrations")} {managerName} +
{bodyText} +
+
+ {_t( + "Integration Managers receive configuration data, and can modify widgets, " + + "send room invites, and set power levels on your behalf.", + )}
- - {_t("Change")} - +
); } } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 78961ad663..b518f7c81b 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {enumerateThemes} from "../../../../../theme"; +import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import PlatformPeg from "../../../../../PlatformPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; @@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -173,11 +174,29 @@ export default class GeneralUserSettingsTab extends React.Component { const newTheme = e.target.value; if (this.state.theme === newTheme) return; - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue('theme'); + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { + dis.dispatch({action: 'recheck_theme'}); + this.setState({theme: oldTheme}); + }); this.setState({theme: newTheme}); - dis.dispatch({action: 'set_theme', value: newTheme}); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); }; + _onUseSystemThemeChanged = (checked) => { + this.setState({useSystemTheme: checked}); + dis.dispatch({action: 'recheck_theme'}); + } + + _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -288,11 +307,24 @@ export default class GeneralUserSettingsTab extends React.Component { _renderThemeSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + + const themeWatcher = new ThemeWatcher(); + let systemThemeSection; + if (themeWatcher.isSystemThemeSupported()) { + systemThemeSection =
+ +
; + } return (
{_t("Theme")} + {systemThemeSection} + value={this.state.theme} onChange={this._onThemeChange} + disabled={this.state.useSystemTheme} + > {Object.entries(enumerateThemes()).map(([theme, text]) => { return ; })} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index fbad327078..875f0bfc10 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/createRoom.js b/src/createRoom.js index 120043247d..0ee90beba8 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -21,7 +21,6 @@ import { _t } from './languageHandler'; import dis from "./dispatcher"; import * as Rooms from "./Rooms"; -import Promise from 'bluebird'; import {getAddressType} from "./UserAddress"; /** diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e60007be5e..9feded09b6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -340,10 +340,10 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", - "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Send verification requests in direct message": "Send verification requests in direct message", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", + "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -364,6 +364,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", + "Match system dark mode setting": "Match system dark mode setting", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", @@ -512,6 +513,9 @@ "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.", "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", + "Connecting to integration manager...": "Connecting to integration manager...", + "Cannot connect to integration manager": "Cannot connect to integration manager", + "The integration manager is offline or it cannot reach your homeserver.": "The integration manager 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.", @@ -599,15 +603,10 @@ "Do not use an identity server": "Do not use an identity server", "Enter a new identity server": "Enter a new identity server", "Change": "Change", - "Failed to update integration manager": "Failed to update integration manager", - "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", - "Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.", - "Integration manager has no terms of service": "Integration manager has no terms of service", - "The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.", - "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", - "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", - "Integration Manager": "Integration Manager", - "Enter a new integration manager": "Enter a new integration manager", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", + "Manage integrations": "Manage integrations", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", @@ -1025,8 +1024,7 @@ "numbered-list": "numbered-list", "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", - "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", + "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1186,10 +1184,19 @@ "Quick Reactions": "Quick Reactions", "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", - "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", - "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", - "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", + "Any of the following data may be shared:": "Any of the following data may be shared:", + "Your display name": "Your display name", + "Your avatar URL": "Your avatar URL", + "Your user ID": "Your user ID", + "Your theme": "Your theme", + "Riot URL": "Riot URL", + "Room ID": "Room ID", + "Widget ID": "Widget ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", + "Widgets do not use message encryption.": "Widgets do not use message encryption.", + "Widget added by": "Widget added by", + "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", @@ -1197,10 +1204,8 @@ "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room", "Minimize apps": "Minimize apps", "Maximize apps": "Maximize apps", - "Reload widget": "Reload widget", "Popout widget": "Popout widget", - "Picture": "Picture", - "Revoke widget access": "Revoke widget access", + "More options": "More options", "Create new room": "Create new room", "Unblacklist": "Unblacklist", "Blacklist": "Blacklist", @@ -1391,6 +1396,10 @@ "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", "Waiting for partner to confirm...": "Waiting for partner to confirm...", "Incoming Verification Request": "Incoming Verification Request", + "Integrations are disabled": "Integrations are disabled", + "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", + "Integrations not allowed": "Integrations not allowed", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Start verification": "Start verification", @@ -1468,7 +1477,7 @@ "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", - "Integrations Manager": "Integrations Manager", + "Integration Manager": "Integration Manager", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", @@ -1497,6 +1506,7 @@ "A widget would like to verify your identity": "A widget would like to verify your identity", "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.": "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.", "Remember my selection for this widget": "Remember my selection for this widget", + "Allow": "Allow", "Deny": "Deny", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", @@ -1556,6 +1566,10 @@ "Hide": "Hide", "Home": "Home", "Sign in": "Sign in", + "Reload": "Reload", + "Take picture": "Take picture", + "Remove for everyone": "Remove for everyone", + "Remove for me": "Remove for me", "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", "Custom Server Options": "Custom Server Options", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index eef9438761..64272bb839 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2337,5 +2337,19 @@ "%(role)s in %(roomName)s": "%(role)s dans %(roomName)s", "Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.", "Security": "Sécurité", - "Verify": "Vérifier" + "Verify": "Vérifier", + "Enable cross-signing to verify per-user instead of per-device": "Activer la signature croisée pour vérifier par utilisateur et non par appareil", + "Any of the following data may be shared:": "Les données suivants peuvent être partagées :", + "Your display name": "Votre nom d’affichage", + "Your avatar URL": "L’URL de votre avatar", + "Your user ID": "Votre identifiant utilisateur", + "Your theme": "Votre thème", + "Riot URL": "URL de Riot", + "Room ID": "Identifiant du salon", + "Widget ID": "Identifiant du widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", + "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", + "Widget added by": "Widget ajouté par", + "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres." } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3c049cc321..892f21dbb1 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2324,5 +2324,19 @@ "%(role)s in %(roomName)s": "%(role)s a szobában: %(roomName)s", "Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.", "Security": "Biztonság", - "Verify": "Ellenőriz" + "Verify": "Ellenőriz", + "Enable cross-signing to verify per-user instead of per-device": "Kereszt-aláírás engedélyezése eszköz alapú ellenőrzés helyett felhasználó alapú ellenőrzéshez", + "Any of the following data may be shared:": "Az alábbi adatok közül bármelyik megosztásra kerülhet:", + "Your display name": "Megjelenítési neved", + "Your avatar URL": "Profilképed URL-je", + "Your user ID": "Felhasználói azonosítód", + "Your theme": "Témád", + "Riot URL": "Riot URL", + "Room ID": "Szoba azonosító", + "Widget ID": "Kisalkalmazás azon.", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.", + "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", + "Widget added by": "A kisalkalmazást hozzáadta", + "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen." } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 8c7edbadd8..efab4595f6 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2270,5 +2270,30 @@ "You cancelled": "Hai annullato", "%(name)s cancelled": "%(name)s ha annullato", "%(name)s wants to verify": "%(name)s vuole verificare", - "You sent a verification request": "Hai inviato una richiesta di verifica" + "You sent a verification request": "Hai inviato una richiesta di verifica", + "Custom (%(level)s)": "Personalizzato (%(level)s)", + "Trusted": "Fidato", + "Not trusted": "Non fidato", + "Hide verified Sign-In's": "Nascondi accessi verificati", + "%(count)s verified Sign-In's|other": "%(count)s accessi verificati", + "%(count)s verified Sign-In's|one": "1 accesso verificato", + "Direct message": "Messaggio diretto", + "Unverify user": "Revoca verifica utente", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.", + "Security": "Sicurezza", + "Verify": "Verifica", + "Enable cross-signing to verify per-user instead of per-device": "Attiva la firma incrociata per verificare per-utente invece che per-dispositivo", + "Any of the following data may be shared:": "Possono essere condivisi tutti i seguenti dati:", + "Your display name": "Il tuo nome visualizzato", + "Your avatar URL": "L'URL del tuo avatar", + "Your user ID": "Il tuo ID utente", + "Your theme": "Il tuo tema", + "Riot URL": "URL di Riot", + "Room ID": "ID stanza", + "Widget ID": "ID widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", + "Using this widget may share data with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s.", + "Widget added by": "Widget aggiunto da", + "This widget may use cookies.": "Questo widget può usare cookie." } diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 8d34fab025..757edbfa4b 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2181,5 +2181,19 @@ "Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.", "Security": "보안", "Verify": "확인", - "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기." + "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "다이렉트 메시지에서 구성원 패널에 새 확인 UX가 적용된 확인 요청을 보냅니다.", + "Enable cross-signing to verify per-user instead of per-device": "기기 당 확인이 아닌 사람 당 확인을 위한 교차 서명 켜기", + "Any of the following data may be shared:": "다음 데이터가 공유됩니다:", + "Your display name": "당신의 표시 이름", + "Your avatar URL": "당신의 아바타 URL", + "Your user ID": "당신의 사용자 ID", + "Your theme": "당신의 테마", + "Riot URL": "Riot URL", + "Room ID": "방 ID", + "Widget ID": "위젯 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.", + "Using this widget may share data with %(widgetDomain)s.": "이 위젯을 사용하면 %(widgetDomain)s와(과) 데이터를 공유합니다.", + "Widget added by": "위젯을 추가했습니다", + "This widget may use cookies.": "이 위젯은 쿠키를 사용합니다." } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 31f82bc2dd..a0ce517404 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -320,7 +320,7 @@ "Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)", "Moderator": "Moderator", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "Name": "Imię", + "Name": "Nazwa", "Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "Never send encrypted messages to unverified devices in this room from this device": "Nigdy nie wysyłaj niezaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "New address (e.g. #foo:%(localDomain)s)": "Nowy adres (np. #foo:%(localDomain)s)", @@ -972,7 +972,7 @@ "Disinvite this user?": "Anulować zaproszenie tego użytkownika?", "Unignore": "Przestań ignorować", "Jump to read receipt": "Przeskocz do potwierdzenia odczytu", - "Share Link to User": "Udostępnij link do użytkownika", + "Share Link to User": "Udostępnij odnośnik do użytkownika", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "W tej chwili nie można odpowiedzieć plikiem, więc zostanie wysłany nie będąc odpowiedzią.", "Unable to reply": "Nie udało się odpowiedzieć", "At this time it is not possible to reply with an emote.": "W tej chwili nie można odpowiedzieć emotikoną.", @@ -1492,7 +1492,7 @@ "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.": "Możesz się zarejestrować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "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.": "Możesz zresetować hasło, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "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.": "Możesz się zalogować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", - "No homeserver URL provided": "Nie podano URL serwera głównego.", + "No homeserver URL provided": "Nie podano URL serwera głównego", "The server does not support the room version specified.": "Serwer nie wspiera tej wersji pokoju.", "Name or Matrix ID": "Imię lub identyfikator Matrix", "Email, name or Matrix ID": "E-mail, imię lub Matrix ID", @@ -1528,7 +1528,7 @@ "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s dezaktywował Flair dla %(groups)s w tym pokoju.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(newGroups)s i dezaktywował Flair dla %(oldGroups)s w tym pokoju.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał zaproszednie dla %(targetDisplayName)s aby dołączył do pokoju.", - "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze.", + "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze…", "Cannot reach homeserver": "Błąd połączenia z serwerem domowym", "Ensure you have a stable internet connection, or get in touch with the server admin": "Upewnij się, że posiadasz stabilne połączenie internetowe lub skontaktuj się z administratorem serwera", "Your Riot is misconfigured": "Twój Riot jest źle skonfigurowany", @@ -1556,7 +1556,7 @@ "Order rooms in the room list by most important first instead of most recent": "Kolejkuj pokoje na liście pokojów od najważniejszych niż od najnowszych", "Show hidden events in timeline": "Pokaż ukryte wydarzenia na linii czasowej", "Low bandwidth mode": "Tryb wolnej przepustowości", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Powzól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Pozwól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", "Messages containing my username": "Wiadomości zawierające moją nazwę użytkownika", "Encrypted messages in one-to-one chats": "Zaszyforwane wiadomości w rozmowach jeden-do-jednego", "Encrypted messages in group chats": "Zaszyfrowane wiadomości w rozmowach grupowych", @@ -1619,7 +1619,7 @@ "Disconnect Identity Server": "Odłącz Serwer Tożsamości", "Disconnect": "Odłącz", "Identity Server (%(server)s)": "Serwer tożsamości (%(server)s)", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz aby odkrywać i być odkrywanym przez isteniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz , aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", "Identity Server": "Serwer Tożsamości", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że nie będzie możliwości wykrycia przez innych użytkowników oraz nie będzie możliwości zaproszenia innych e-mailem lub za pomocą telefonu.", @@ -1653,5 +1653,84 @@ "You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", "Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju", "Use an identity server": "Użyj serwera tożsamości", - "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów" + "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów", + "Trust": "Zaufaj", + "Custom (%(level)s)": "Własny (%(level)s)", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Użyj serwera tożsamości, by zaprosić z użyciem adresu e-mail. Kliknij dalej, żeby użyć domyślnego serwera tożsamości (%(defaultIdentityServerName)s), lub zmień w Ustawieniach.", + "Use an identity server to invite by email. Manage in Settings.": "Użyj serwera tożsamości, by zaprosić za pomocą adresu e-mail. Zarządzaj w ustawieniach.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Użyj nowego, spójnego panelu informacji o użytkowniku dla członków pokoju i grup", + "Try out new ways to ignore people (experimental)": "Wypróbuj nowe sposoby na ignorowanie ludzi (eksperymentalne)", + "Send verification requests in direct message": "Wysyłaj prośby o weryfikację w bezpośredniej wiadomości", + "Use the new, faster, composer for writing messages": "Używaj nowego, szybszego kompozytora do pisania wiadomości", + "My Ban List": "Moja lista zablokowanych", + "This is your list of users/servers you have blocked - don't leave the room!": "To jest Twoja lista zablokowanych użytkowników/serwerów – nie opuszczaj tego pokoju!", + "Change identity server": "Zmień serwer tożsamości", + "Disconnect from the identity server and connect to instead?": "Rozłączyć się z serwerem tożsamości i połączyć się w jego miejsce z ?", + "Disconnect identity server": "Odłączanie serwera tożsamości", + "You should:": "Należy:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "sprawdzić rozszerzenia przeglądarki, które mogą blokować serwer tożsamości (takie jak Privacy Badger)", + "contact the administrators of identity server ": "skontaktować się z administratorami serwera tożsamości ", + "wait and try again later": "zaczekaj i spróbuj ponownie później", + "Disconnect anyway": "Odłącz mimo to", + "You are still sharing your personal data on the identity server .": "W dalszym ciągu udostępniasz swoje dane osobowe na serwerze tożsamości .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Zalecamy, by usunąć swój adres e-mail i numer telefonu z serwera tożsamości przed odłączeniem.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jeżeli nie chcesz używać do odnajdywania i bycia odnajdywanym przez osoby, które znasz, wpisz inny serwer tożsamości poniżej.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Używanie serwera tożsamości jest opcjonalne. Jeżeli postanowisz nie używać serwera tożsamości, pozostali użytkownicy nie będą w stanie Cię odnaleźć ani nie będziesz mógł zaprosić innych po adresie e-mail czy numerze telefonu.", + "Do not use an identity server": "Nie używaj serwera tożsamości", + "Clear cache and reload": "Wyczyść pamięć podręczną i przeładuj", + "Something went wrong. Please try again or view your console for hints.": "Coś poszło nie tak. Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Please verify the room ID or alias and try again.": "Zweryfikuj poprawność ID pokoju lub nazwy zastępczej i spróbuj ponownie.", + "Please try again or view your console for hints.": "Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Personal ban list": "Osobista lista zablokowanych", + "Server or user ID to ignore": "ID serwera lub użytkownika do zignorowania", + "eg: @bot:* or example.org": "np: @bot:* lub przykład.pl", + "Composer": "Kompozytor", + "Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)", + "Explore": "Przeglądaj", + "Filter": "Filtruj", + "Add room": "Dodaj pokój", + "A device's public name is visible to people you communicate with": "Publiczna nazwa urządzenia jest widoczna dla ludzi, z którymi się komunikujesz", + "Request media permissions": "Zapytaj o uprawnienia", + "Voice & Video": "Głos & Wideo", + "this room": "ten pokój", + "View older messages in %(roomName)s.": "Wyświetl starsze wiadomości w %(roomName)s.", + "Room information": "Informacje o pokoju", + "Internal room ID:": "Wewnętrzne ID pokoju:", + "Uploaded sound": "Przesłano dźwięk", + "Change history visibility": "Zmień widoczność historii", + "Upgrade the room": "Zaktualizuj pokój", + "Enable room encryption": "Włącz szyfrowanie pokoju", + "Select the roles required to change various parts of the room": "Wybierz role wymagane do zmieniania różnych części pokoju", + "Enable encryption?": "Włączyć szyfrowanie?", + "Your email address hasn't been verified yet": "Twój adres e-mail nie został jeszcze zweryfikowany", + "Verification code": "Kod weryfikacyjny", + "Remove %(email)s?": "Usunąć %(email)s?", + "Remove %(phone)s?": "Usunąć %(phone)s?", + "Some devices in this encrypted room are not trusted": "Niektóre urządzenia w tym zaszyfrowanym pokoju nie są zaufane", + "Loading …": "Ładowanie…", + "Loading room preview": "Wczytywanie podglądu pokoju", + "Try to join anyway": "Spróbuj dołączyć mimo tego", + "You can still join it because this is a public room.": "Możesz mimo to dołączyć, gdyż pokój jest publiczny.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "To zaproszenie do %(roomName)s zostało wysłane na adres %(email)s, który nie jest przypisany do Twojego konta", + "Link this email with your account in Settings to receive invites directly in Riot.": "Połącz ten adres e-mail z Twoim kontem w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "This invite to %(roomName)s was sent to %(email)s": "To zaproszenie do %(roomName)s zostało wysłane do %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Użyj serwera tożsamości w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "Do you want to chat with %(user)s?": "Czy chcesz rozmawiać z %(user)s?", + "Do you want to join %(roomName)s?": "Czy chcesz dołączyć do %(roomName)s?", + " invited you": " zaprosił(a) CIę", + "You're previewing %(roomName)s. Want to join it?": "Przeglądasz %(roomName)s. Czy chcesz dołączyć do pokoju?", + "Not now": "Nie teraz", + "Don't ask me again": "Nie pytaj ponownie", + "%(count)s unread messages including mentions.|other": "%(count)s nieprzeczytanych wiadomości, wliczając wzmianki.", + "%(count)s unread messages including mentions.|one": "1 nieprzeczytana wzmianka.", + "%(count)s unread messages.|other": "%(count)s nieprzeczytanych wiadomości.", + "%(count)s unread messages.|one": "1 nieprzeczytana wiadomość.", + "Unread mentions.": "Nieprzeczytane wzmianki.", + "Unread messages.": "Nieprzeczytane wiadomości.", + "Join": "Dołącz", + "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", + "Preview": "Przejrzyj", + "View": "Wyświetl", + "Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać." } diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 7cc80cfc78..5a56e807e4 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -842,5 +842,11 @@ "Collapse panel": "Colapsar o painel", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Com o seu navegador atual, a aparência e sensação de uso da aplicação podem estar completamente incorretas, e algumas das funcionalidades poderão não funcionar. Se quiser tentar de qualquer maneira pode continuar, mas está por sua conta com algum problema que possa encontrar!", "Checking for an update...": "A procurar uma atualização...", - "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui" + "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui", + "Add Email Address": "Adicione adresso de e-mail", + "Add Phone Number": "Adicione número de telefone", + "The platform you're on": "A plataforma em que se encontra", + "The version of Riot.im": "A versão do RIOT.im", + "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)", + "Your language of choice": "O seu idioma de escolha" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 01065a9e96..7806ea731b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2018,7 +2018,7 @@ "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", "Make this room public": "Сделать комнату публичной", - "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений", "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", @@ -2050,7 +2050,7 @@ "contact the administrators of identity server ": "связаться с администраторами сервера идентификации ", "wait and try again later": "Подождите и повторите попытку позже", "Error changing power level requirement": "Ошибка изменения требования к уровню прав", - "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню прав комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню доступа комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Error changing power level": "Ошибка изменения уровня прав", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении уровня прав пользователя. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Unable to revoke sharing for email address": "Не удается отменить общий доступ к адресу электронной почты", @@ -2165,5 +2165,14 @@ "%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.", "%(count)s unread messages.|one": "1 непрочитанное сообщение.", "Unread messages.": "Непрочитанные сообщения.", - "Message Actions": "Сообщение действий" + "Message Actions": "Сообщение действий", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Это действие требует по умолчанию доступа к серверу идентификации для подтверждения адреса электронной почты или номера телефона, но у сервера нет никакого пользовательского соглашения.", + "Custom (%(level)s)": "Пользовательский (%(level)s)", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Try out new ways to ignore people (experimental)": "Попробуйте новые способы игнорировать людей (экспериментальные)", + "Send verification requests in direct message": "Отправить запросы на подтверждение в прямом сообщении", + "My Ban List": "Мой список запрещенных", + "Ignored/Blocked": "Игнорируемые/Заблокированные", + "Error adding ignored user/server": "Ошибка добавления игнорируемого пользователя/сервера", + "Error subscribing to list": "Ошибка при подписке на список" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 2bf5732131..4d0ad6582b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2292,5 +2292,17 @@ "%(role)s in %(roomName)s": "%(role)s në %(roomName)s", "Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.", "Security": "Siguri", - "Verify": "Verifikoje" + "Verify": "Verifikoje", + "Any of the following data may be shared:": "Mund të ndahen me të tjerët cilado prej të dhënave vijuese:", + "Your display name": "Emri juaj në ekran", + "Your avatar URL": "URL-ja e avatarit tuaj", + "Your user ID": "ID-ja juaj e përdoruesit", + "Your theme": "Tema juaj", + "Riot URL": "URL Riot-i", + "Room ID": "ID dhome", + "Widget ID": "ID widget-i", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.", + "Using this widget may share data with %(widgetDomain)s.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s.", + "Widget added by": "Widget i shtuar nga", + "This widget may use cookies.": "Ky widget mund të përdorë cookies." } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5c6e69c864..1dfdc34f1a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2330,5 +2330,19 @@ "%(role)s in %(roomName)s": "%(role)s 在 %(roomName)s", "Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。", "Security": "安全", - "Verify": "驗證" + "Verify": "驗證", + "Send verification requests in direct message, including a new verification UX in the member panel.": "在直接訊息中傳送驗證請求,包含成員面板中新的驗證使用者體驗。", + "Enable cross-signing to verify per-user instead of per-device": "啟用交叉簽章以驗證每個使用者而非每個裝置", + "Any of the following data may be shared:": "可能會分享以下資料:", + "Your display name": "您的顯示名稱", + "Your avatar URL": "您的大頭貼 URL", + "Your user ID": "您的使用 ID", + "Your theme": "您的佈景主題", + "Riot URL": "Riot URL", + "Room ID": "聊天室 ID", + "Widget ID": "小工具 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", + "Using this widget may share data with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 。", + "Widget added by": "小工具新增由", + "This widget may use cookies.": "這個小工具可能會使用 cookies。" } diff --git a/src/indexing/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.js new file mode 100644 index 0000000000..5e8ca668ad --- /dev/null +++ b/src/indexing/BaseEventIndexManager.js @@ -0,0 +1,223 @@ +/* +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. +*/ + +export interface MatrixEvent { + type: string; + sender: string; + content: {}; + event_id: string; + origin_server_ts: number; + unsigned: ?{}; + room_id: string; +} + +export interface MatrixProfile { + avatar_url: string; + displayname: string; +} + +export interface CrawlerCheckpoint { + roomId: string; + token: string; + fullCrawl: boolean; + direction: string; +} + +export interface ResultContext { + events_before: [MatrixEvent]; + events_after: [MatrixEvent]; + profile_info: Map; +} + +export interface ResultsElement { + rank: number; + result: MatrixEvent; + context: ResultContext; +} + +export interface SearchResult { + count: number; + results: [ResultsElement]; + highlights: [string]; +} + +export interface SearchArgs { + search_term: string; + before_limit: number; + after_limit: number; + order_by_recency: boolean; + room_id: ?string; +} + +export interface HistoricEvent { + event: MatrixEvent; + profile: MatrixProfile; +} + +/** + * Base class for classes that provide platform-specific event indexing. + * + * Instances of this class are provided by the application. + */ +export default class BaseEventIndexManager { + /** + * Does our EventIndexManager support event indexing. + * + * If an EventIndexManager implementor has runtime dependencies that + * optionally enable event indexing they may override this method to perform + * the necessary runtime checks here. + * + * @return {Promise} A promise that will resolve to true if event indexing + * is supported, false otherwise. + */ + async supportsEventIndexing(): Promise { + return true; + } + /** + * Initialize the event index for the given user. + * + * @return {Promise} A promise that will resolve when the event index is + * initialized. + */ + async initEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Queue up an event to be added to the index. + * + * @param {MatrixEvent} ev The event that should be added to the index. + * @param {MatrixProfile} profile The profile of the event sender at the + * time of the event receival. + * + * @return {Promise} A promise that will resolve when the was queued up for + * addition. + */ + async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Check if our event index is empty. + */ + indexIsEmpty(): Promise { + throw new Error("Unimplemented"); + } + + /** + * Commit the previously queued up events to the index. + * + * @return {Promise} A promise that will resolve once the queued up events + * were added to the index. + */ + async commitLiveEvents(): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Search the event index using the given term for matching events. + * + * @param {SearchArgs} searchArgs The search configuration sets what should + * be searched for and what should be contained in the search result. + * + * @return {Promise<[SearchResult]>} A promise that will resolve to an array + * of search results once the search is done. + */ + async searchEventIndex(searchArgs: SearchArgs): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add events from the room history to the event index. + * + * This is used to add a batch of events to the index. + * + * @param {[HistoricEvent]} events The list of events and profiles that + * should be added to the event index. + * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that + * should be stored in the index which should be used to continue crawling + * the room. + * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used + * to fetch the current batch of events. This checkpoint will be removed + * from the index. + * + * @return {Promise} A promise that will resolve to true if all the events + * were already added to the index, false otherwise. + */ + async addHistoricEvents( + events: [HistoricEvent], + checkpoint: CrawlerCheckpoint | null, + oldCheckpoint: CrawlerCheckpoint | null, + ): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added + * to the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been stored. + */ + async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be + * removed from the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been removed. + */ + async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Load the stored checkpoints from the index. + * + * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an + * array of crawler checkpoints once they have been loaded from the index. + */ + async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { + throw new Error("Unimplemented"); + } + + /** + * close our event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been closed. + */ + async closeEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Delete our current event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been deleted. + */ + async deleteEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } +} diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js new file mode 100644 index 0000000000..6bad992017 --- /dev/null +++ b/src/indexing/EventIndex.js @@ -0,0 +1,412 @@ +/* +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 PlatformPeg from "../PlatformPeg"; +import MatrixClientPeg from "../MatrixClientPeg"; + +/* + * Event indexing class that wraps the platform specific event indexing. + */ +export default class EventIndex { + constructor() { + this.crawlerCheckpoints = []; + // The time that the crawler will wait between /rooms/{room_id}/messages + // requests + this._crawlerTimeout = 3000; + // The maximum number of events our crawler should fetch in a single + // crawl. + this._eventsPerCrawl = 100; + this._crawler = null; + this.liveEventsForIndex = new Set(); + } + + async init() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + await indexManager.initEventIndex(); + + this.registerListeners(); + } + + registerListeners() { + const client = MatrixClientPeg.get(); + + client.on('sync', this.onSync); + client.on('Room.timeline', this.onRoomTimeline); + client.on('Event.decrypted', this.onEventDecrypted); + client.on('Room.timelineReset', this.onTimelineReset); + } + + removeListeners() { + const client = MatrixClientPeg.get(); + if (client === null) return; + + client.removeListener('sync', this.onSync); + client.removeListener('Room.timeline', this.onRoomTimeline); + client.removeListener('Event.decrypted', this.onEventDecrypted); + client.removeListener('Room.timelineReset', this.onTimelineReset); + } + + onSync = async (state, prevState, data) => { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + if (prevState === null && state === "PREPARED") { + // Load our stored checkpoints, if any. + this.crawlerCheckpoints = await indexManager.loadCheckpoints(); + console.log("EventIndex: Loaded checkpoints", + this.crawlerCheckpoints); + return; + } + + if (prevState === "PREPARED" && state === "SYNCING") { + const addInitialCheckpoints = async () => { + const client = MatrixClientPeg.get(); + const rooms = client.getRooms(); + + const isRoomEncrypted = (room) => { + return client.isRoomEncrypted(room.roomId); + }; + + // We only care to crawl the encrypted rooms, non-encrypted. + // rooms can use the search provided by the homeserver. + const encryptedRooms = rooms.filter(isRoomEncrypted); + + console.log("EventIndex: Adding initial crawler checkpoints"); + + // Gather the prev_batch tokens and create checkpoints for + // our message crawler. + await Promise.all(encryptedRooms.map(async (room) => { + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + console.log("EventIndex: Got token for indexer", + room.roomId, token); + + const backCheckpoint = { + roomId: room.roomId, + token: token, + direction: "b", + }; + + const forwardCheckpoint = { + roomId: room.roomId, + token: token, + direction: "f", + }; + + await indexManager.addCrawlerCheckpoint(backCheckpoint); + await indexManager.addCrawlerCheckpoint(forwardCheckpoint); + this.crawlerCheckpoints.push(backCheckpoint); + this.crawlerCheckpoints.push(forwardCheckpoint); + })); + }; + + // If our indexer is empty we're most likely running Riot the + // first time with indexing support or running it with an + // initial sync. Add checkpoints to crawl our encrypted rooms. + const eventIndexWasEmpty = await indexManager.isEventIndexEmpty(); + if (eventIndexWasEmpty) await addInitialCheckpoints(); + + // Start our crawler. + this.startCrawler(); + return; + } + + if (prevState === "SYNCING" && state === "SYNCING") { + // A sync was done, presumably we queued up some live events, + // commit them now. + console.log("EventIndex: Committing events"); + await indexManager.commitLiveEvents(); + return; + } + } + + onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + // We only index encrypted rooms locally. + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + // If it isn't a live event or if it's redacted there's nothing to + // do. + if (toStartOfTimeline || !data || !data.liveEvent + || ev.isRedacted()) { + return; + } + + // If the event is not yet decrypted mark it for the + // Event.decrypted callback. + if (ev.isBeingDecrypted()) { + const eventId = ev.getId(); + this.liveEventsForIndex.add(eventId); + } else { + // If the event is decrypted or is unencrypted add it to the + // index now. + await this.addLiveEventToIndex(ev); + } + } + + onEventDecrypted = async (ev, err) => { + const eventId = ev.getId(); + + // If the event isn't in our live event set, ignore it. + if (!this.liveEventsForIndex.delete(eventId)) return; + if (err) return; + await this.addLiveEventToIndex(ev); + } + + async addLiveEventToIndex(ev) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + if (["m.room.message", "m.room.name", "m.room.topic"] + .indexOf(ev.getType()) == -1) { + return; + } + + const e = ev.toJSON().decrypted; + const profile = { + displayname: ev.sender.rawDisplayName, + avatar_url: ev.sender.getMxcAvatarUrl(), + }; + + indexManager.addEventToIndex(e, profile); + } + + async crawlerFunc() { + // TODO either put this in a better place or find a library provided + // method that does this. + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + let cancelled = false; + + console.log("EventIndex: Started crawler function"); + + const client = MatrixClientPeg.get(); + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + this._crawler = {}; + + this._crawler.cancel = () => { + cancelled = true; + }; + + while (!cancelled) { + // This is a low priority task and we don't want to spam our + // homeserver with /messages requests so we set a hefty timeout + // here. + await sleep(this._crawlerTimeout); + + console.log("EventIndex: Running the crawler loop."); + + if (cancelled) { + break; + } + + const checkpoint = this.crawlerCheckpoints.shift(); + + /// There is no checkpoint available currently, one may appear if + // a sync with limited room timelines happens, so go back to sleep. + if (checkpoint === undefined) { + continue; + } + + console.log("EventIndex: crawling using checkpoint", checkpoint); + + // We have a checkpoint, let us fetch some messages, again, very + // conservatively to not bother our homeserver too much. + const eventMapper = client.getEventMapper(); + // TODO we need to ensure to use member lazy loading with this + // request so we get the correct profiles. + let res; + + try { + res = await client._createMessagesRequest( + checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, + checkpoint.direction); + } catch (e) { + console.log("EventIndex: Error crawling events:", e); + this.crawlerCheckpoints.push(checkpoint); + continue; + } + + if (res.chunk.length === 0) { + console.log("EventIndex: Done with the checkpoint", checkpoint); + // We got to the start/end of our timeline, lets just + // delete our checkpoint and go back to sleep. + await indexManager.removeCrawlerCheckpoint(checkpoint); + continue; + } + + // Convert the plain JSON events into Matrix events so they get + // decrypted if necessary. + const matrixEvents = res.chunk.map(eventMapper); + let stateEvents = []; + if (res.state !== undefined) { + stateEvents = res.state.map(eventMapper); + } + + const profiles = {}; + + stateEvents.forEach(ev => { + if (ev.event.content && + ev.event.content.membership === "join") { + profiles[ev.event.sender] = { + displayname: ev.event.content.displayname, + avatar_url: ev.event.content.avatar_url, + }; + } + }); + + const decryptionPromises = []; + + matrixEvents.forEach(ev => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name + decryptionPromises.push(ev._decryptionPromise); + } + }); + + // Let us wait for all the events to get decrypted. + await Promise.all(decryptionPromises); + + // We filter out events for which decryption failed, are redacted + // or aren't of a type that we know how to index. + const isValidEvent = (value) => { + return ([ + "m.room.message", + "m.room.name", + "m.room.topic", + ].indexOf(value.getType()) >= 0 + && !value.isRedacted() && !value.isDecryptionFailure() + ); + // TODO do we need to check if the event has all the valid + // attributes? + }; + + // TODO if there are no events at this point we're missing a lot + // decryption keys, do we want to retry this checkpoint at a later + // stage? + const filteredEvents = matrixEvents.filter(isValidEvent); + + // Let us convert the events back into a format that EventIndex can + // consume. + const events = filteredEvents.map((ev) => { + const jsonEvent = ev.toJSON(); + + let e; + if (ev.isEncrypted()) e = jsonEvent.decrypted; + else e = jsonEvent; + + let profile = {}; + if (e.sender in profiles) profile = profiles[e.sender]; + const object = { + event: e, + profile: profile, + }; + return object; + }); + + // Create a new checkpoint so we can continue crawling the room for + // messages. + const newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + + console.log( + "EventIndex: Crawled room", + client.getRoom(checkpoint.roomId).name, + "and fetched", events.length, "events.", + ); + + try { + const eventsAlreadyAdded = await indexManager.addHistoricEvents( + events, newCheckpoint, checkpoint); + // If all events were already indexed we assume that we catched + // up with our index and don't need to crawl the room further. + // Let us delete the checkpoint in that case, otherwise push + // the new checkpoint to be used by the crawler. + if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + console.log("EventIndex: Checkpoint had already all events", + "added, stopping the crawl", checkpoint); + await indexManager.removeCrawlerCheckpoint(newCheckpoint); + } else { + this.crawlerCheckpoints.push(newCheckpoint); + } + } catch (e) { + console.log("EventIndex: Error durring a crawl", e); + // An error occurred, put the checkpoint back so we + // can retry. + this.crawlerCheckpoints.push(checkpoint); + } + } + + this._crawler = null; + + console.log("EventIndex: Stopping crawler function"); + } + + onTimelineReset = async (room, timelineSet, resetAllTimelines) => { + if (room === null) return; + + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + const backwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "b", + }; + + console.log("EventIndex: Added checkpoint because of a limited timeline", + backwardsCheckpoint); + + await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); + + this.crawlerCheckpoints.push(backwardsCheckpoint); + } + + startCrawler() { + if (this._crawler !== null) return; + this.crawlerFunc(); + } + + stopCrawler() { + if (this._crawler === null) return; + this._crawler.cancel(); + } + + async close() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + this.removeListeners(); + this.stopCrawler(); + return indexManager.closeEventIndex(); + } + + async search(searchArgs) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + return indexManager.searchEventIndex(searchArgs); + } +} diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js new file mode 100644 index 0000000000..c0bdd74ff4 --- /dev/null +++ b/src/indexing/EventIndexPeg.js @@ -0,0 +1,115 @@ +/* +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. +*/ + +/* + * Object holding the global EventIndex object. Can only be initialized if the + * platform supports event indexing. + */ + +import PlatformPeg from "../PlatformPeg"; +import EventIndex from "../indexing/EventIndex"; +import SettingsStore from '../settings/SettingsStore'; + +class EventIndexPeg { + constructor() { + this.index = null; + } + + /** + * Create a new EventIndex and initialize it if the platform supports it. + * + * @return {Promise} A promise that will resolve to true if an + * EventIndex was successfully initialized, false otherwise. + */ + async init() { + if (!SettingsStore.isFeatureEnabled("feature_event_indexing")) { + return false; + } + + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (!indexManager || await indexManager.supportsEventIndexing() !== true) { + console.log("EventIndex: Platform doesn't support event indexing,", + "not initializing."); + return false; + } + + const index = new EventIndex(); + + try { + await index.init(); + } catch (e) { + console.log("EventIndex: Error initializing the event index", e); + return false; + } + + console.log("EventIndex: Successfully initialized the event index"); + + this.index = index; + + return true; + } + + /** + * Get the current event index. + * + * @return {EventIndex} The current event index. + */ + get() { + return this.index; + } + + stop() { + if (this.index === null) return; + this.index.stopCrawler(); + } + + /** + * Unset our event store + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * closed. + */ + async unset() { + if (this.index === null) return; + this.index.close(); + this.index = null; + } + + /** + * Delete our event indexer. + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * deleted. + */ + async deleteEventIndex() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + if (indexManager !== null) { + this.unset(); + console.log("EventIndex: Deleting event index."); + await indexManager.deleteEventIndex(); + } + } +} + +if (!global.mxEventIndexPeg) { + global.mxEventIndexPeg = new EventIndexPeg(); +} +module.exports = global.mxEventIndexPeg; diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index d36fa73d48..4958209351 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -20,6 +20,8 @@ import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; import url from 'url'; +import SettingsStore from "../settings/SettingsStore"; +import {IntegrationManagers} from "./IntegrationManagers"; export const KIND_ACCOUNT = "account"; export const KIND_CONFIG = "config"; @@ -57,19 +59,23 @@ export class IntegrationManagerInstance { } async open(room: Room = null, screen: string = null, integrationId: string = null): void { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + if (!SettingsStore.getValue("integrationProvisioning")) { + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - {loading: true}, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + {loading: true}, 'mx_IntegrationManager', ); const client = this.getScalarClient(); client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -94,8 +100,8 @@ export class IntegrationManagerInstance { // Close the old dialog and open a new one dialog.close(); Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - newProps, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + newProps, 'mx_IntegrationManager', ); } } diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index a0fbff56fb..6c4d2ae4d4 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -22,6 +22,7 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; +import SettingsStore from "../settings/SettingsStore"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours const KIND_PREFERENCE = [ @@ -172,15 +173,19 @@ export class IntegrationManagers { } openNoManagerDialog(): void { - // TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken. - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - Modal.createTrackedDialog( - "Integration Manager", "None", IntegrationsManager, - {configured: false}, 'mx_IntegrationsManager', - ); + const IntegrationsImpossibleDialog = sdk.getComponent("dialogs.IntegrationsImpossibleDialog"); + Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog); } openAll(room: Room = null, screen: string = null, integrationId: string = null): void { + if (!SettingsStore.getValue("integrationProvisioning")) { + return this.showDisabledDialog(); + } + + if (this._managers.length === 0) { + return this.openNoManagerDialog(); + } + const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog"); Modal.createTrackedDialog( 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog, @@ -188,6 +193,11 @@ export class IntegrationManagers { ); } + showDisabledDialog(): void { + const IntegrationsDisabledDialog = sdk.getComponent("dialogs.IntegrationsDisabledDialog"); + Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog); + } + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { // TODO: TravisR - We should be logging out of scalar clients. await WidgetUtils.removeIntegrationManagerWidgets(); diff --git a/src/languageHandler.js b/src/languageHandler.js index 179bb2d1d0..c56e5378df 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -19,7 +19,6 @@ limitations under the License. import request from 'browser-request'; import counterpart from 'counterpart'; -import Promise from 'bluebird'; import React from 'react'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 820550af88..47bab38079 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - // This module contains all the code needed to log the console, persist it to // disk and submit bug reports. Rationale is as follows: // - Monkey-patching the console is preferable to having a log library because diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index e772912e48..457958eb82 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -17,7 +17,6 @@ limitations under the License. */ import pako from 'pako'; -import Promise from 'bluebird'; import MatrixClientPeg from '../MatrixClientPeg'; import PlatformPeg from '../PlatformPeg'; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89bca043bd..89693f7c50 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,12 +120,6 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_user_info_panel": { - isFeature: true, - displayName: _td("Use the new, consistent UserInfo panel for Room Members and Group Members"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_mjolnir": { isFeature: true, displayName: _td("Try out new ways to ignore people (experimental)"), @@ -142,7 +136,8 @@ export const SETTINGS = { }, "feature_dm_verification": { isFeature: true, - displayName: _td("Send verification requests in direct message"), + displayName: _td("Send verification requests in direct message," + + " including a new verification UX in the member panel."), supportedLevels: LEVELS_FEATURE, default: false, }, @@ -153,6 +148,12 @@ export const SETTINGS = { default: false, controller: new ReloadOnChangeController(), }, + "feature_event_indexing": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Enable local event indexing and E2EE search (requires restart)"), + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, @@ -280,6 +281,11 @@ export const SETTINGS = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: [], }, + "use_system_theme": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: true, + displayName: _td("Match system dark mode setting"), + }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Allow Peer-to-Peer for 1:1 calls'), diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index f738bf7971..7b05ad0c1b 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -126,6 +126,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa if (!content || !content['recent_rooms']) { content = this._getSettings(BREADCRUMBS_LEGACY_EVENT_TYPE); } + if (!content) content = {}; // If we still don't have content, make some content['recent_rooms'] = newValue; return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content); diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index 780815efd1..76c518b97b 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from "../../MatrixClientPeg"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js index e6964f9bf7..4cbe4891be 100644 --- a/src/settings/handlers/LocalEchoWrapper.js +++ b/src/settings/handlers/LocalEchoWrapper.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; import SettingsHandler from "./SettingsHandler"; /** diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js index a0981ffbab..a9cf686c4c 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.js +++ b/src/settings/handlers/RoomDeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js index d1566d6bfa..7d987fc136 100644 --- a/src/settings/handlers/SettingsHandler.js +++ b/src/settings/handlers/SettingsHandler.js @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - /** * Represents the base class for all level handlers. This class performs no logic * and should be overridden. diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index c8b4d75010..94b81c1ba5 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -15,7 +15,6 @@ limitations under the License. */ import EventEmitter from 'events'; -import Promise from 'bluebird'; const BULK_REQUEST_DEBOUNCE_MS = 200; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 6a405124f4..a3caf876ef 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -234,7 +234,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).done(() => { + ).then(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/src/theme.js b/src/theme.js index 8a15c606d7..92bf03ef0a 100644 --- a/src/theme.js +++ b/src/theme.js @@ -19,8 +19,75 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; +import dis from "./dispatcher"; import SettingsStore from "./settings/SettingsStore"; +export class ThemeWatcher { + static _instance = null; + + constructor() { + this._themeWatchRef = null; + this._systemThemeWatchRef = null; + this._dispatcherRef = null; + + // we have both here as each may either match or not match, so by having both + // we can get the tristate of dark/light/unsupported + this._preferDark = global.matchMedia("(prefers-color-scheme: dark)"); + this._preferLight = global.matchMedia("(prefers-color-scheme: light)"); + + this._currentTheme = this.getEffectiveTheme(); + } + + start() { + this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); + this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + this._dispatcherRef = dis.register(this._onAction); + } + + stop() { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + SettingsStore.unwatchSetting(this._systemThemeWatchRef); + SettingsStore.unwatchSetting(this._themeWatchRef); + dis.unregister(this._dispatcherRef); + } + + _onChange = () => { + this.recheck(); + } + + _onAction = (payload) => { + if (payload.action === 'recheck_theme') { + // XXX forceTheme + this.recheck(payload.forceTheme); + } + } + + // XXX: forceTheme param aded here as local echo appears to be unreliable + // https://github.com/vector-im/riot-web/issues/11443 + recheck(forceTheme) { + const oldTheme = this._currentTheme; + this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; + if (oldTheme !== this._currentTheme) { + setTheme(this._currentTheme); + } + } + + getEffectiveTheme() { + if (SettingsStore.getValue('use_system_theme')) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + return SettingsStore.getValue('theme'); + } + + isSystemThemeSupported() { + return this._preferDark.matches || this._preferLight.matches; + } +} + export function enumerateThemes() { const BUILTIN_THEMES = { "light": _t("Light theme"), @@ -60,30 +127,17 @@ function getCustomTheme(themeName) { return customTheme; } -/** - * Gets the underlying theme name for the given theme. This is usually the theme or - * CSS resource that the theme relies upon to load. - * @param {string} theme The theme name to get the base of. - * @returns {string} The base theme (typically "light" or "dark"). - */ -export function getBaseTheme(theme) { - if (!theme) return "light"; - if (theme.startsWith("custom-")) { - const customTheme = getCustomTheme(theme.substr(7)); - return customTheme.is_dark ? "dark-custom" : "light-custom"; - } - - return theme; // it's probably a base theme -} - /** * Called whenever someone changes the theme + * Async function that returns once the theme has been set + * (ie. the CSS has been loaded) * * @param {string} theme new theme */ -export function setTheme(theme) { +export async function setTheme(theme) { if (!theme) { - theme = SettingsStore.getValue("theme"); + const themeWatcher = new ThemeWatcher(); + theme = themeWatcher.getEffectiveTheme(); } let stylesheetName = theme; if (theme.startsWith("custom-")) { @@ -122,38 +176,41 @@ export function setTheme(theme) { styleElements[stylesheetName].disabled = false; - const switchTheme = function() { - // we re-enable our theme here just in case we raced with another - // theme set request as per https://github.com/vector-im/riot-web/issues/5601. - // We could alternatively lock or similar to stop the race, but - // this is probably good enough for now. - styleElements[stylesheetName].disabled = false; - Object.values(styleElements).forEach((a) => { - if (a == styleElements[stylesheetName]) return; - a.disabled = true; - }); - Tinter.setTheme(theme); - }; + return new Promise((resolve) => { + const switchTheme = function() { + // we re-enable our theme here just in case we raced with another + // theme set request as per https://github.com/vector-im/riot-web/issues/5601. + // We could alternatively lock or similar to stop the race, but + // this is probably good enough for now. + styleElements[stylesheetName].disabled = false; + Object.values(styleElements).forEach((a) => { + if (a == styleElements[stylesheetName]) return; + a.disabled = true; + }); + Tinter.setTheme(theme); + resolve(); + }; - // turns out that Firefox preloads the CSS for link elements with - // the disabled attribute, but Chrome doesn't. + // turns out that Firefox preloads the CSS for link elements with + // the disabled attribute, but Chrome doesn't. - let cssLoaded = false; + let cssLoaded = false; - styleElements[stylesheetName].onload = () => { - switchTheme(); - }; + styleElements[stylesheetName].onload = () => { + switchTheme(); + }; - for (let i = 0; i < document.styleSheets.length; i++) { - const ss = document.styleSheets[i]; - if (ss && ss.href === styleElements[stylesheetName].href) { - cssLoaded = true; - break; + for (let i = 0; i < document.styleSheets.length; i++) { + const ss = document.styleSheets[i]; + if (ss && ss.href === styleElements[stylesheetName].href) { + cssLoaded = true; + break; + } } - } - if (cssLoaded) { - styleElements[stylesheetName].onload = undefined; - switchTheme(); - } + if (cssLoaded) { + styleElements[stylesheetName].onload = undefined; + switchTheme(); + } + }); } diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index ea0e4c3fb0..f193bd7709 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -21,7 +21,6 @@ import encrypt from 'browser-encrypt-attachment'; import 'isomorphic-fetch'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import MatrixClientPeg from '../MatrixClientPeg'; -import Promise from 'bluebird'; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index de5c2e7610..8b952a2b5b 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -15,11 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; import GroupStore from '../stores/GroupStore'; -import Promise from 'bluebird'; import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 36907da5ab..9bab78dee4 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -400,7 +400,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId, app, sender, roomId) { + static makeAppConfig(appId, app, senderUserId, roomId, eventId) { const myUserId = MatrixClientPeg.get().credentials.userId; const user = MatrixClientPeg.get().getUser(myUserId); const params = { @@ -413,7 +413,13 @@ export default class WidgetUtils { '$theme': SettingsStore.getValue("theme"), }; + if (!senderUserId) { + throw new Error("Widgets must be created by someone - provide a senderUserId"); + } + app.creatorUserId = senderUserId; + app.id = appId; + app.eventId = eventId; app.name = app.name || app.type; if (app.data) { @@ -425,7 +431,6 @@ export default class WidgetUtils { } app.url = encodeUri(app.url, params); - app.creatorUserId = (sender && sender.userId) ? sender.userId : null; return app; } diff --git a/src/utils/promise.js b/src/utils/promise.js index e6e6ccb5c8..d7e8d2eae1 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// This is only here to allow access to methods like done for the time being -import Promise from "bluebird"; - // @flow // Returns a promise which resolves with a given value after the given number of ms diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 7612b43b48..5f90e0f21c 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -15,7 +15,6 @@ limitations under the License. */ import expect from 'expect'; -import Promise from 'bluebird'; import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 95f7e7999a..a31cbdebb5 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 04a5c83ed0..60380eecd2 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -3,7 +3,6 @@ import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import sinon from 'sinon'; -import Promise from 'bluebird'; import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); diff --git a/test/components/views/rooms/RoomSettings-test.js b/test/components/views/rooms/RoomSettings-test.js index dd91e812bc..1c0bfd95dc 100644 --- a/test/components/views/rooms/RoomSettings-test.js +++ b/test/components/views/rooms/RoomSettings-test.js @@ -3,7 +3,6 @@ // import ReactDOM from 'react-dom'; // import expect from 'expect'; // import jest from 'jest-mock'; -// import Promise from 'bluebird'; // import * as testUtils from '../../../test-utils'; // import sdk from 'matrix-react-sdk'; // const WrappedRoomSettings = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.rooms.RoomSettings')); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 0d96bc15ab..8f21638703 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); }); afterEach(function() { diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js index be598de8da..77dfb37b0a 100644 --- a/test/stores/RoomViewStore-test.js +++ b/test/stores/RoomViewStore-test.js @@ -1,13 +1,11 @@ import expect from 'expect'; -import dis from '../../src/dispatcher'; import RoomViewStore from '../../src/stores/RoomViewStore'; import peg from '../../src/MatrixClientPeg'; import * as testUtils from '../test-utils'; -import Promise from 'bluebird'; const dispatch = testUtils.getDispatchForStore(RoomViewStore); diff --git a/test/test-utils.js b/test/test-utils.js index ff800132b9..64704fc610 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,7 +1,6 @@ "use strict"; import sinon from 'sinon'; -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..0aff8ce793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -244,6 +244,11 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -293,6 +298,28 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/experimental-utils@^2.5.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" + integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.8.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/typescript-estree@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" + integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -899,7 +926,7 @@ babel-helper-explode-assignable-expression@^6.24.1: babel-traverse "^6.24.1" babel-types "^6.24.1" -babel-helper-function-name@^6.24.1, babel-helper-function-name@^6.8.0: +babel-helper-function-name@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= @@ -1042,16 +1069,6 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= -babel-plugin-transform-async-to-bluebird@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz#46ea3e7c5af629782ac9f1ed1b7cd38f8425afd4" - integrity sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q= - dependencies: - babel-helper-function-name "^6.8.0" - babel-plugin-syntax-async-functions "^6.8.0" - babel-template "^6.9.0" - babel-traverse "^6.10.4" - babel-plugin-transform-async-to-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" @@ -1442,7 +1459,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtim core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-template@^6.9.0: +babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= @@ -1453,7 +1470,7 @@ babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-tem babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.10.4, babel-traverse@^6.24.1, babel-traverse@^6.26.0: +babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= @@ -2891,6 +2908,13 @@ eslint-plugin-flowtype@^2.30.0: dependencies: lodash "^4.17.10" +eslint-plugin-jest@^23.0.4: + version "23.0.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0" + integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw== + dependencies: + "@typescript-eslint/experimental-utils" "^2.5.0" + eslint-plugin-react-hooks@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756" @@ -2924,6 +2948,14 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1: version "1.4.2" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" @@ -2931,7 +2963,7 @@ eslint-utils@^1.3.1: dependencies: eslint-visitor-keys "^1.0.0" -eslint-visitor-keys@^1.0.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== @@ -3714,6 +3746,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -5038,6 +5082,11 @@ lodash.mergewith@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -7149,7 +7198,7 @@ selection-is-backward@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0: +semver@6.3.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -8086,11 +8135,18 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.4.tgz#3b52b1f13924f460c3fbfd0df69b587dbcbc762e" integrity sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q== -tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"