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 @@
-
-
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("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 {
;
- } 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 (
-