Merge branch 'develop' into jryans/4s-new-key-backup

This commit is contained in:
Travis Ralston 2019-11-21 10:22:13 -07:00
commit 72f0401589
142 changed files with 2121 additions and 807 deletions

View file

@ -13,7 +13,6 @@
],
"transform-class-properties",
"transform-object-rest-spread",
"transform-async-to-bluebird",
"transform-runtime",
"add-module-exports",
"syntax-dynamic-import"

View file

@ -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.

View file

@ -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.

View file

@ -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",

View file

@ -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;
}

View file

@ -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";

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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');

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="11.014242"
height="12"
viewBox="0 0 11.014242 12"
version="1.1"
id="svg6"
sodipodi:docname="bin.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="750"
inkscape:window-height="480"
id="namedview8"
showgrid="false"
fit-margin-top="0.5"
fit-margin-left="0.5"
fit-margin-bottom="0.5"
fit-margin-right="0.5"
inkscape:zoom="19.666667"
inkscape:cx="5.5071212"
inkscape:cy="6"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<g
id="g4"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
transform="translate(1.0071212)">
<path
d="M 0,3 H 9 M 8,3 v 7 A 1,1 0 0 1 7,11 H 2 A 1,1 0 0 1 1,10 V 3 M 2.5,3 V 2 a 1,1 0 0 1 1,-1 h 2 a 1,1 0 0 1 1,1 v 1 m -3,2.5 v 3 m 2,-3 v 3"
id="path2"
style="stroke:#000000;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="11" viewBox="0 0 13 11">
<g fill="none" fill-rule="evenodd" stroke="#212121" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<path d="M11 8a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V2.5a1 1 0 0 1 1-1h2L4 0h3l1 1.5h2a1 1 0 0 1 1 1V8z"/>
<circle cx="5.5" cy="5" r="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 378 B

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="11" viewBox="0 0 12 11">
<g fill="none" fill-rule="evenodd" stroke="#212121" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 6.33V9a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h2.67"/>
<path d="M9 0l2 2-5 5H4V5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 324 B

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="11" viewBox="0 0 13 11">
<g fill="none" fill-rule="evenodd" stroke="#212121" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1.5v3H9M1 9.5v-3h3"/>
<path d="M2.255 4A4.5 4.5 0 0 1 9.68 2.32L12 4.5m-11 2l2.32 2.18A4.5 4.5 0 0 0 10.745 7"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 346 B

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g fill="none" fill-rule="evenodd" stroke="#212121" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<circle cx="5" cy="5" r="5"/>
<path d="M6.5 3.5l-3 3M3.5 3.5l3 3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 315 B

View file

@ -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;
}
}

View file

@ -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 = {

View file

@ -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';

View file

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

View file

@ -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";

View file

@ -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

View file

@ -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,

View file

@ -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';

View file

@ -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,

View file

@ -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');

View file

@ -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,
});

138
src/Searching.js Normal file
View file

@ -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);
}

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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";

View file

@ -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 = {

View file

@ -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() {

View file

@ -121,7 +121,7 @@ export default createReactClass({
this.setState({
errorText: msg,
});
}).done();
});
this._intervalId = null;
if (this.props.poll) {

View file

@ -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'});

View file

@ -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') {

View file

@ -185,7 +185,7 @@ export default class RightPanel extends React.Component {
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} 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 = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
} 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",

View file

@ -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 {

View file

@ -289,7 +289,7 @@ module.exports = createReactClass({
}
return <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ title }
@ -306,7 +306,7 @@ module.exports = createReactClass({
if (this._shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ _t('Connectivity to the server has been lost.') }

View file

@ -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,

View file

@ -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";

View file

@ -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);

View file

@ -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,
});

View file

@ -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) {

View file

@ -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,

View file

@ -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);

View file

@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
});
},
/*

View file

@ -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(() => {

View file

@ -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(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
{_t("Edit")}
</AccessibleButton>,
);
}
if (this.props.onReloadClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
key='reload'>
{_t("Reload")}
</AccessibleButton>,
);
}
if (this.props.onSnapshotClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
key='snap'>
{_t("Take picture")}
</AccessibleButton>,
);
}
if (this.props.onDeleteClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
key='delete'>
{_t("Remove for everyone")}
</AccessibleButton>,
);
}
// Push this last so it appears last. It's always present.
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</AccessibleButton>,
);
// Put separators between the options
if (options.length > 1) {
const length = options.length;
for (let i = 0; i < length - 1; i++) {
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
// 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 <div className="mx_WidgetContextMenu">{options}</div>;
}
}

View file

@ -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,
});

View file

@ -93,7 +93,7 @@ export default createReactClass({
this.setState({createError: e});
}).finally(() => {
this.setState({creating: false});
}).done();
});
},
_onCancel: function() {

View file

@ -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 (
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Integrations are disabled")}>
<div className='mx_IntegrationsDisabledDialog_content'>
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
</div>
<DialogButtons
primaryButton={_t("Settings")}
onPrimaryButtonClick={this._onOpenSettingsClick}
cancelButton={_t("OK")}
onCancel={this._onAcknowledgeClick}
/>
</BaseDialog>
);
}
}

View file

@ -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 (
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
onFinished={this.props.onFinished}
title={_t("Integrations not allowed")}>
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
"Please contact an admin.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this._onAcknowledgeClick}
hasCancel={false}
/>
</BaseDialog>
);
}
}

View file

@ -78,7 +78,7 @@ export default createReactClass({
true,
);
}
}).done();
});
},
componentWillUnmount: function() {

View file

@ -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});

View file

@ -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';

View file

@ -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 <IntegrationsManager
return <IntegrationManager
configured={true}
loading={this.state.currentLoading}
connected={this.state.currentConnected}

View file

@ -86,7 +86,7 @@ export default class TermsDialog extends React.PureComponent {
case Matrix.SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICE_TYPES.IM:
return <div>{_t("Integrations Manager")}<br />({host})</div>;
return <div>{_t("Integration Manager")}<br />({host})</div>;
}
}

View file

@ -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 =
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
}
const cookieWarning =
<span className='mx_AppPermissionWarningTextLabel'>
{ _t('Warning: This widget might use cookies.') }
</span>;
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
? <MemberAvatar member={this.state.roomMember} width={38} height={38} />
: <BaseAvatar name={this.props.creatorUserId} width={38} height={38} />;
const warningTooltipText = (
<div>
{_t("Any of the following data may be shared:")}
<ul>
<li>{_t("Your display name")}</li>
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("Riot URL")}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>
</div>
);
const warningTooltip = (
<TextWithTooltip tooltip={warningTooltipText} tooltipClass='mx_AppPermissionWarning_tooltip mx_Tooltip_dark'>
<span className='mx_AppPermissionWarning_helpIcon' />
</TextWithTooltip>
);
// 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 <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip})
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip});
const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null;
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
{_t("Widget added by")}
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span>
<span className='mx_AppPermissionWarningTextURL'
title={this.state.curlBase}
>{this.state.curlBase}</span>
{ e2eWarningText }
{ cookieWarning }
<div className='mx_AppPermissionWarning_row'>
{avatar}
<h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
<div className='mx_AppPermissionWarning_smallText'>{userId}</div>
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{warning}
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div className='mx_AppPermissionWarning_row'>
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
<input
className='mx_AppPermissionButton'
type='button'
value={_t('Allow')}
onClick={this.props.onPermissionGranted}
/>
</div>
);
}
}
AppPermission.propTypes = {
isRoomEncrypted: PropTypes.bool,
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
isRoomEncrypted: false,
onPermissionGranted: function() {},
};

View file

@ -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 {
</div>
);
if (!this.state.hasPermissionToLoad) {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this.state.widgetUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
@ -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() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Reload widget */ }
{ this.props.showReload && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_reload"
title={_t('Reload widget')}
onClick={this._onReloadWidgetClick}
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
/> }
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_snapshot"
title={_t('Picture')}
onClick={this._onSnapshotClick}
/> }
{ /* Edit widget */ }
{ showEditButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_edit"
title={_t('Edit')}
onClick={this._onEditClick}
/> }
{ /* Delete widget */ }
{ showDeleteButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_delete"
title={_t('Delete widget')}
onClick={this._onDeleteClick}
/> }
{ /* Cancel widget */ }
{ showCancelButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_cancel"
title={_t('Revoke widget access')}
onClick={this._onCancelClick}
{ /* Context menu */ }
{ <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
title={_t('More options')}
onClick={this._onContextMenuClick}
/> }
</span>
</div> }
@ -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,

View file

@ -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({

View file

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

View file

@ -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();
});
},
});
};

View file

@ -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

View file

@ -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 <AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}

View file

@ -21,7 +21,8 @@ import sdk from '../../../index';
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
tooltip: PropTypes.string.isRequired,
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
};
constructor() {
@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component {
<Tooltip
label={this.props.tooltip}
visible={this.state.hover}
tooltipClassName={this.props.tooltipClass}
className={"mx_TextWithTooltip_tooltip"} />
</span>
);

View file

@ -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);

View file

@ -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,

View file

@ -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

View file

@ -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();
});
}
},

View file

@ -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);

View file

@ -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';

View file

@ -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 (<AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}

View file

@ -21,7 +21,6 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import { Room } from 'matrix-js-sdk';
import SettingsStore from "../../../settings/SettingsStore";

View file

@ -53,7 +53,7 @@ module.exports = createReactClass({
);
}, (error)=>{
console.error("Failed to get URL preview: " + error);
}).done();
});
},
componentDidMount: function() {

View file

@ -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() {

View file

@ -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 (
<div className="mx_MessageComposer">
<div className={wrapperClasses}>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{ controls }
</div>

View file

@ -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 (
<div className="mx_MessageComposer">
<div className={wrapperClasses}>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{ controls }
</div>

View file

@ -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

View file

@ -112,7 +112,7 @@ module.exports = createReactClass({
}
});
httpPromise.done(function() {
httpPromise.then(function() {
self.setState({
phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),

View file

@ -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() {

View file

@ -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 || []});

View file

@ -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 (
<div className='mx_IntegrationsManager_error'>
<h3>{_t("No integrations server configured")}</h3>
<p>{_t("This Riot instance does not have an integrations server configured.")}</p>
</div>
);
}
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className='mx_IntegrationsManager_loading'>
<h3>{_t("Connecting to integrations server...")}</h3>
<div className='mx_IntegrationManager_loading'>
<h3>{_t("Connecting to integration manager...")}</h3>
<Spinner />
</div>
);
@ -91,9 +78,9 @@ export default class IntegrationsManager extends React.Component {
if (!this.props.connected) {
return (
<div className='mx_IntegrationsManager_error'>
<h3>{_t("Cannot connect to integrations server")}</h3>
<p>{_t("The integrations server is offline or it cannot reach your homeserver.")}</p>
<div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3>
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
</div>
);
}

View file

@ -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}));
},

View file

@ -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 <div>
<InlineSpinner />
{ _t("Checking server") }
</div>;
} else if (this.state.error) {
return <span className="warning">{this.state.error}</span>;
} 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: (
<div>
<span className="warning">
{_t("The integration manager you have chosen does not have any terms of service.")}
</span>
<span>
&nbsp;{_t("Only continue if you trust the owner of the server.")}
</span>
</div>
),
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 <b>%(serverName)s</b> to manage your bots, widgets, " +
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{serverName: currentManager.name},
{ b: sub => <b>{sub}</b> },
);
} 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 (
<form className="mx_SettingsTab_section mx_SetIntegrationManager" onSubmit={this._setManager}>
<div className='mx_SetIntegrationManager'>
<div className="mx_SettingsTab_heading">
<span>{_t("Integration Manager")}</span>
<span>{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
<ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
</div>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
<br />
<br />
{_t(
"Integration Managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
</span>
<Field
label={_t("Enter a new integration manager")}
id="mx_SetIntegrationManager_newUrl"
type="text" value={this.state.url}
autoComplete="off"
onChange={this._onUrlChanged}
tooltipContent={this._getTooltip()}
tooltipClassName="mx_SetIntegrationManager_tooltip"
disabled={this.state.busy}
flagInvalid={!!this.state.error}
/>
<AccessibleButton
kind="primary_sm"
type="submit"
disabled={!this._canChange()}
onClick={this._setManager}
>{_t("Change")}</AccessibleButton>
</form>
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show more