{ _t('Connectivity to the server has been lost.') }
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 4de573479d..5cc1e2b765 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';
@@ -357,7 +357,7 @@ module.exports = createReactClass({
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (!room && shouldPeek) {
- console.log("Attempting to peek into room %s", roomId);
+ console.info("Attempting to peek into room %s", roomId);
this.setState({
peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
@@ -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,
@@ -1907,7 +1896,7 @@ module.exports = createReactClass({
highlightedEventId = this.state.initialEventId;
}
- // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
+ // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
{
+ this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
const { events, liveEvents } = this._getEvents();
@@ -1064,8 +1063,6 @@ const TimelinePanel = createReactClass({
});
};
- let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
-
// if we already have the event in question, TimelineWindow.load
// returns a resolved promise.
//
@@ -1074,9 +1071,14 @@ const TimelinePanel = createReactClass({
// quite slow. So we detect that situation and shortcut straight to
// calling _reloadEvents and updating the state.
- if (prom.isFulfilled()) {
+ const timeline = this.props.timelineSet.getTimelineForEvent(eventId);
+ 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);
this.setState({
events: [],
liveEvents: [],
@@ -1084,11 +1086,8 @@ const TimelinePanel = createReactClass({
canForwardPaginate: false,
timelineLoading: true,
});
-
- prom = prom.then(onLoaded, onError);
+ prom.then(onLoaded, onError);
}
-
- prom.done();
},
// handle the completion of a timeline load or localEchoUpdate, by
diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js
new file mode 100644
index 0000000000..a8dca35747
--- /dev/null
+++ b/src/components/structures/ToastContainer.js
@@ -0,0 +1,84 @@
+/*
+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 * as React from "react";
+import dis from "../../dispatcher";
+import { _t } from '../../languageHandler';
+import classNames from "classnames";
+
+export default class ToastContainer extends React.Component {
+ constructor() {
+ super();
+ this.state = {toasts: []};
+ }
+
+ componentDidMount() {
+ this._dispatcherRef = dis.register(this.onAction);
+ }
+
+ componentWillUnmount() {
+ dis.unregister(this._dispatcherRef);
+ }
+
+ onAction = (payload) => {
+ if (payload.action === "show_toast") {
+ this._addToast(payload.toast);
+ }
+ };
+
+ _addToast(toast) {
+ this.setState({toasts: this.state.toasts.concat(toast)});
+ }
+
+ dismissTopToast = () => {
+ const [, ...remaining] = this.state.toasts;
+ this.setState({toasts: remaining});
+ };
+
+ render() {
+ const totalCount = this.state.toasts.length;
+ const isStacked = totalCount > 1;
+ let toast;
+ if (totalCount !== 0) {
+ const topToast = this.state.toasts[0];
+ const {title, icon, key, component, props} = topToast;
+ const toastClasses = classNames("mx_Toast_toast", {
+ "mx_Toast_hasIcon": icon,
+ [`mx_Toast_icon_${icon}`]: icon,
+ });
+ const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
+
+ const toastProps = Object.assign({}, props, {
+ dismiss: this.dismissTopToast,
+ key,
+ });
+ toast = (
+
{title}{countIndicator}
+
{React.createElement(component, toastProps)}
+
);
+ }
+
+ const containerClasses = classNames("mx_ToastContainer", {
+ "mx_ToastContainer_stacked": isStacked,
+ });
+
+ return (
+
+ {toast}
+
+ );
+ }
+}
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 ad77ed49a5..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) {
@@ -378,15 +378,30 @@ module.exports = createReactClass({
// Do a quick liveliness check on the URLs
try {
- await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
- this.setState({serverIsAlive: true, errorText: ""});
+ const { warning } =
+ await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
+ if (warning) {
+ this.setState({
+ ...AutoDiscoveryUtils.authComponentStateForError(warning),
+ errorText: "",
+ });
+ } else {
+ this.setState({
+ serverIsAlive: true,
+ errorText: "",
+ });
+ }
} catch (e) {
this.setState({
busy: false,
...AutoDiscoveryUtils.authComponentStateForError(e),
});
if (this.state.serverErrorIsFatal) {
- return; // Server is dead - do not continue.
+ // Server is dead: show server details prompt instead
+ this.setState({
+ phase: PHASE_SERVER_DETAILS,
+ });
+ return;
}
}
@@ -424,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/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js
index 2fdfedadcf..23fef6c54f 100644
--- a/src/components/views/auth/CaptchaForm.js
+++ b/src/components/views/auth/CaptchaForm.js
@@ -89,7 +89,7 @@ module.exports = createReactClass({
+ "authentication");
}
- console.log("Rendering to %s", divId);
+ console.info("Rendering to %s", divId);
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
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 e00e3dcdbb..f770bb0d60 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -17,7 +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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@@ -32,6 +31,7 @@ import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
+import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextualMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
@@ -92,7 +92,7 @@ module.exports = createReactClass({
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
- Promise.delay(500).then(() => {
+ sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
@@ -149,7 +149,7 @@ module.exports = createReactClass({
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
- ).delay(500).finally(() => {
+ ).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
@@ -190,7 +190,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) {
@@ -220,10 +220,10 @@ 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 Promise.delay(500).then(() => {
+ return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
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 fb779fa96f..a40495893d 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -25,13 +25,13 @@ import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
-import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
+import {sleep} from "../../../utils/promise";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -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,
});
@@ -533,7 +533,7 @@ module.exports = createReactClass({
};
// wait a bit to let the user finish typing
- await Promise.delay(500);
+ await sleep(500);
if (cancelled) return null;
try {
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/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
index 6014cb941c..b5e4daa1c1 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -116,7 +116,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
nodes.push((
{
+ 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/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index fb9045f05a..d3ab2b8722 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,12 +30,34 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
+import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
+ constructor() {
+ super();
+
+ this.state = {
+ mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"),
+ };
+ }
+
+ componentDidMount(): void {
+ this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
+ }
+
+ componentWillUnmount(): void {
+ SettingsStore.unwatchSetting(this._mjolnirWatcher);
+ }
+
+ _mjolnirChanged(settingName, roomId, atLevel, newValue) {
+ // We can cheat because we know what levels a feature is tracked at, and how it is tracked
+ this.setState({mjolnirEnabled: newValue});
+ }
+
_getTabs() {
const tabs = [];
@@ -75,6 +98,13 @@ export default class UserSettingsDialog extends React.Component {
,
));
}
+ if (this.state.mjolnirEnabled) {
+ tabs.push(new Tab(
+ _td("Ignored users"),
+ "mx_UserSettingsDialog_mjolnirIcon",
+
,
+ ));
+ }
tabs.push(new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
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}
+
+
-
);
}
}
-
-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..9a29843d3b 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -34,7 +34,9 @@ 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";
+import PersistedElement from "./PersistedElement";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -52,7 +54,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 +71,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 +83,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 +209,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));
@@ -244,7 +248,8 @@ export default class AppTile extends React.Component {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
- if (this.props.waitForIframeLoad) {
+ // We assume that persisted widgets are loaded and don't need a spinner.
+ if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({
loading: true,
});
@@ -269,7 +274,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 +296,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 +363,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.info("Revoke widget permissions - %s", this.props.id);
+ this._revokeWidgetPermission();
}
/**
@@ -446,24 +447,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 +543,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 +605,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 +625,14 @@ export default class AppTile extends React.Component {
);
if (!this.state.hasPermissionToLoad) {
- const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
+ const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
@@ -596,12 +654,7 @@ export default class AppTile extends React.Component {
appTileBody = (
{ this.state.loading && loadingElement }
- { /*
- The "is" attribute in the following iframe tag is needed in order to enable rendering of the
- "allow" attribute, which is unknown to react 15.
- */ }
}
@@ -720,6 +742,7 @@ AppTile.displayName ='AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
+ eventId: PropTypes.string, // required for room widgets
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js
index 3bf37df951..5cba98470c 100644
--- a/src/components/views/elements/EditableTextContainer.js
+++ b/src/components/views/elements/EditableTextContainer.js
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
-import Promise from 'bluebird';
/**
* A component which wraps an EditableText, with a spinner while updates take
@@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component {
this.setState({busy: true});
- this.props.getInitialValue().done(
+ this.props.getInitialValue().then(
(result) => {
if (this._unmounted) { return; }
this.setState({
@@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component {
errorString: null,
});
- this.props.onSubmit(value).done(
+ this.props.onSubmit(value).then(
() => {
if (this._unmounted) { return; }
this.setState({
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js
index e53e1ec0fa..e36464c4ef 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.js
@@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
- MatrixClientPeg.get().store.deleteAllData().done(() => {
+ MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
};
diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js
new file mode 100644
index 0000000000..f6b4c986f5
--- /dev/null
+++ b/src/components/views/elements/FormButton.js
@@ -0,0 +1,28 @@
+/*
+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 AccessibleButton from "./AccessibleButton";
+
+export default function FormButton(props) {
+ const {className, label, kind, ...restProps} = props;
+ const newClassName = (className || "") + " mx_FormButton";
+ const allProps = Object.assign({}, restProps,
+ {className: newClassName, kind: kind || "primary", children: [label]});
+ return React.createElement(AccessibleButton, allProps);
+}
+
+FormButton.propTypes = AccessibleButton.propTypes;
diff --git a/src/components/views/elements/GroupsButton.js b/src/components/views/elements/GroupsButton.js
index 3932c827c5..7b15e96424 100644
--- a/src/components/views/elements/GroupsButton.js
+++ b/src/components/views/elements/GroupsButton.js
@@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
-
{
this.setState({langs: ['en']});
- }).done();
+ });
if (!this.props.value) {
// If no value is given, we start with the first
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index d6931850be..19e4be6083 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -67,13 +67,15 @@ module.exports = createReactClass({
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
});
const app = WidgetUtils.makeAppConfig(
- appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
+ appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
+ persistentWidgetInRoomId, appEvent.getId(),
);
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
const AppTile = sdk.getComponent('elements.AppTile');
return
);
@@ -151,7 +152,7 @@ module.exports = createReactClass({
picker = (
{options}
diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js
index 61c3a2125a..f6cef47117 100644
--- a/src/components/views/elements/TextWithTooltip.js
+++ b/src/components/views/elements/TextWithTooltip.js
@@ -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 {
);
diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js
index bb5f9f0604..8ff3ce9bdb 100644
--- a/src/components/views/elements/Tooltip.js
+++ b/src/components/views/elements/Tooltip.js
@@ -100,7 +100,9 @@ module.exports = createReactClass({
const parent = ReactDOM.findDOMNode(this).parentNode;
let style = {};
style = this._updatePosition(style);
- style.display = "block";
+ // Hide the entire container when not visible. This prevents flashing of the tooltip
+ // if it is not meant to be visible on first mount.
+ style.display = this.props.visible ? "block" : "none";
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
"mx_Tooltip_visible": this.props.visible,
diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js
index 7d80bdd209..3cd5731b99 100644
--- a/src/components/views/groups/GroupUserSettings.js
+++ b/src/components/views/groups/GroupUserSettings.js
@@ -36,7 +36,7 @@ export default createReactClass({
},
componentWillMount: function() {
- this.context.matrixClient.getJoinedGroups().done((result) => {
+ this.context.matrixClient.getJoinedGroups().then((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {
console.error(err);
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index b4f26d0cbd..0246d28542 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component {
decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return URL.createObjectURL(decryptedBlob);
- }).done((url) => {
+ }).then((url) => {
this.setState({
decryptedUrl: url,
decryptedBlob: decryptedBlob,
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 640baa1966..b12957a7df 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -24,7 +24,6 @@ import MFileBody from './MFileBody';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile';
-import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@@ -289,7 +288,7 @@ export default class MImageBody extends React.Component {
this.setState({
error: err,
});
- }).done();
+ });
}
// Remember that the user wanted to show this particular image
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js
index 21d82309ed..b2a1724fc6 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.js
@@ -111,10 +111,10 @@ export default class MKeyVerificationRequest extends React.Component {
userLabelForEventRoom(fromUserId, mxEvent)});
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
if (isResolved) {
- const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
+ const FormButton = sdk.getComponent("elements.FormButton");
stateNode = (
-
{_t("Decline")}
-
{_t("Accept")}
+
+
);
}
} else if (isOwn) { // request sent by us
diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js
index d277b6eae9..44954344ff 100644
--- a/src/components/views/messages/MVideoBody.js
+++ b/src/components/views/messages/MVideoBody.js
@@ -20,7 +20,6 @@ import createReactClass from 'create-react-class';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile } from '../../../utils/DecryptFile';
-import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@@ -89,7 +88,7 @@ module.exports = createReactClass({
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
- if (content.info.thumbnail_file) {
+ if (content.info && content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
@@ -115,7 +114,7 @@ module.exports = createReactClass({
this.setState({
error: err,
});
- }).done();
+ });
}
},
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js
index a616dd96ed..e75bcc4332 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.js
@@ -18,6 +18,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
+import SettingsStore from "../../../settings/SettingsStore";
+import {Mjolnir} from "../../../mjolnir/Mjolnir";
module.exports = createReactClass({
displayName: 'MessageEvent',
@@ -49,6 +51,10 @@ module.exports = createReactClass({
return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
},
+ onTileUpdate: function() {
+ this.forceUpdate();
+ },
+
render: function() {
const UnknownBody = sdk.getComponent('messages.UnknownBody');
@@ -81,6 +87,21 @@ module.exports = createReactClass({
}
}
+ if (SettingsStore.isFeatureEnabled("feature_mjolnir")) {
+ const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
+ const allowRender = localStorage.getItem(key) === "true";
+
+ if (!allowRender) {
+ const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':');
+ const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender());
+ const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain);
+
+ if (userBanned || serverBanned) {
+ BodyType = sdk.getComponent('messages.MjolnirBody');
+ }
+ }
+ }
+
return
;
+ onHeightChanged={this.props.onHeightChanged}
+ onMessageAllowed={this.onTileUpdate}
+ />;
},
});
diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js
new file mode 100644
index 0000000000..baaee91657
--- /dev/null
+++ b/src/components/views/messages/MjolnirBody.js
@@ -0,0 +1,48 @@
+/*
+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';
+
+export default class MjolnirBody extends React.Component {
+ static propTypes = {
+ mxEvent: PropTypes.object.isRequired,
+ onMessageAllowed: PropTypes.func.isRequired,
+ };
+
+ constructor() {
+ super();
+ }
+
+ _onAllowClick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
+ localStorage.setItem(key, "true");
+ this.props.onMessageAllowed();
+ };
+
+ render() {
+ return (
+
{_t(
+ "You have ignored this user, so their message is hidden. Show anyways.",
+ {}, {a: (sub) => {sub}},
+ )}
+ );
+ }
+}
diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js
index b70724d516..d7e1ef3488 100644
--- a/src/components/views/messages/ReactionsRowButtonTooltip.js
+++ b/src/components/views/messages/ReactionsRowButtonTooltip.js
@@ -43,7 +43,8 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
if (room) {
const senders = [];
for (const reactionEvent of reactionEvents) {
- const { name } = room.getMember(reactionEvent.getSender());
+ const member = room.getMember(reactionEvent.getSender());
+ const name = member ? member.name : reactionEvent.getSender();
senders.push(name);
}
const shortName = unicodeToShortcode(content);
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index a9e0da143e..dab03cd537 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -144,7 +144,7 @@ module.exports = createReactClass({
},
shouldComponentUpdate: function(nextProps, nextState) {
- //console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
+ //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
@@ -159,7 +159,7 @@ module.exports = createReactClass({
},
calculateUrlPreview: function() {
- //console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
+ //console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);
diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index 207bf29998..41eb723c79 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -27,7 +27,6 @@ import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
-import Unread from '../../../Unread';
import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
@@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import {textualPowerLevel} from '../../../Roles';
const _disambiguateDevices = (devices) => {
const names = Object.create(null);
@@ -63,10 +63,92 @@ const _getE2EStatus = (devices) => {
return hasUnverifiedDevice ? "warning" : "verified";
};
-const DevicesSection = ({devices, userId, loading}) => {
- const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
+async function unverifyUser(matrixClient, userId) {
+ const devices = await matrixClient.getStoredDevicesForUser(userId);
+ for (const device of devices) {
+ if (device.isVerified()) {
+ matrixClient.setDeviceVerified(
+ userId, device.deviceId, false,
+ );
+ }
+ }
+}
+
+function openDMForUser(matrixClient, userId) {
+ const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
+ const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
+ const room = matrixClient.getRoom(roomId);
+ if (!room || room.getMyMembership() === "leave") {
+ return lastActiveRoom;
+ }
+ if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
+ return room;
+ }
+ return lastActiveRoom;
+ }, null);
+
+ if (lastActiveRoom) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: lastActiveRoom.roomId,
+ });
+ } else {
+ createRoom({dmUserId: userId});
+ }
+}
+
+function useIsEncrypted(cli, room) {
+ const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId));
+
+ const update = useCallback((event) => {
+ if (event.getType() === "m.room.encryption") {
+ setIsEncrypted(cli.isRoomEncrypted(room.roomId));
+ }
+ }, [cli, room]);
+ useEventEmitter(room.currentState, "RoomState.events", update);
+ return isEncrypted;
+}
+
+function verifyDevice(userId, device) {
+ const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
+ Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
+ userId: userId,
+ device: device,
+ });
+}
+
+function DeviceItem({userId, device}) {
+ const classes = classNames("mx_UserInfo_device", {
+ mx_UserInfo_device_verified: device.isVerified(),
+ mx_UserInfo_device_unverified: !device.isVerified(),
+ });
+ const iconClasses = classNames("mx_E2EIcon", {
+ mx_E2EIcon_verified: device.isVerified(),
+ mx_E2EIcon_warning: !device.isVerified(),
+ });
+
+ const onDeviceClick = () => {
+ if (!device.isVerified()) {
+ verifyDevice(userId, device);
+ }
+ };
+
+ const deviceName = device.ambiguous ?
+ (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
+ device.getDisplayName();
+ const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted");
+ return (
+
+ {deviceName}
+ {trustedLabel}
+ );
+}
+
+function DevicesSection({devices, userId, loading}) {
const Spinner = sdk.getComponent("elements.Spinner");
+ const [isExpanded, setExpanded] = useState(false);
+
if (loading) {
// still loading
return
;
@@ -74,123 +156,50 @@ const DevicesSection = ({devices, userId, loading}) => {
if (devices === null) {
return _t("Unable to load device list");
}
- if (devices.length === 0) {
- return _t("No devices with registered encryption keys");
- }
- return (
-
-
{ _t("Trust & Devices") }
-
- { devices.map((device, i) => ) }
-
-
- );
-};
+ const unverifiedDevices = devices.filter(d => !d.isVerified());
+ const verifiedDevices = devices.filter(d => d.isVerified());
-const onRoomTileClick = (roomId) => {
- dis.dispatch({
- action: 'view_room',
- room_id: roomId,
- });
-};
-
-const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => {
- const onNewDMClick = async () => {
- startUpdating();
- await createRoom({dmUserId: userId});
- stopUpdating();
- };
-
- // TODO: Immutable DMs replaces a lot of this
- // dmRooms will not include dmRooms that we have been invited into but did not join.
- // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
- // XXX: we potentially want DMs we have been invited to, to also show up here :L
- // especially as logic below concerns specially if we haven't joined but have been invited
- const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId));
-
- // TODO bind the below
- // cli.on("Room", this.onRoom);
- // cli.on("Room.name", this.onRoomName);
- // cli.on("deleteRoom", this.onDeleteRoom);
-
- const accountDataHandler = useCallback((ev) => {
- if (ev.getType() === "m.direct") {
- const dmRoomMap = new DMRoomMap(cli);
- setDmRooms(dmRoomMap.getDMRoomsForUserId(userId));
- }
- }, [cli, userId]);
- useEventEmitter(cli, "accountData", accountDataHandler);
-
- const RoomTile = sdk.getComponent("rooms.RoomTile");
-
- const tiles = [];
- for (const roomId of dmRooms) {
- const room = cli.getRoom(roomId);
- if (room) {
- const myMembership = room.getMyMembership();
- // not a DM room if we have are not joined
- if (myMembership !== 'join') continue;
-
- const them = room.getMember(userId);
- // not a DM room if they are not joined
- if (!them || !them.membership || them.membership !== 'join') continue;
-
- const highlight = room.getUnreadNotificationCount('highlight') > 0;
-
- tiles.push(
-
,
- );
+ let expandButton;
+ if (verifiedDevices.length) {
+ if (isExpanded) {
+ expandButton = (
setExpanded(false)}>
+ {_t("Hide verified Sign-In's")}
+ );
+ } else {
+ expandButton = (
setExpanded(true)}>
+
+ {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}
+ );
}
}
- const labelClasses = classNames({
- mx_UserInfo_createRoom_label: true,
- mx_RoomTile_name: true,
+ let deviceList = unverifiedDevices.map((device, i) => {
+ return (
);
});
-
- let body = tiles;
- if (!body) {
- body = (
-
-
-
})
-
- { _t("Start a chat") }
-
- );
+ if (isExpanded) {
+ const keyStart = unverifiedDevices.length;
+ deviceList = deviceList.concat(verifiedDevices.map((device, i) => {
+ return (
);
+ }));
}
return (
-
-
-
{ _t("Direct messages") }
-
-
- { body }
+
+
{deviceList}
+
{expandButton}
);
-});
+}
-const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => {
+const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
let ignoreButton = null;
let insertPillButton = null;
let inviteUserButton = null;
let readReceiptButton = null;
+ const isMe = member.userId === cli.getUserId();
+
const onShareUserClick = () => {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
@@ -200,7 +209,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
// Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt
- if (member.userId !== cli.getUserId()) {
+ if (!isMe) {
const onIgnoreToggle = () => {
const ignoredUsers = cli.getIgnoredUsers();
if (isIgnored) {
@@ -214,7 +223,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
};
ignoreButton = (
-
+
{ isIgnored ? _t("Unignore") : _t("Ignore") }
);
@@ -285,15 +294,34 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
);
+ let directMessageButton;
+ if (!isMe) {
+ directMessageButton = (
+
openDMForUser(cli, member.userId)} className="mx_UserInfo_field">
+ { _t('Direct message') }
+
+ );
+ }
+ let unverifyButton;
+ if (devices && devices.some(device => device.isVerified())) {
+ unverifyButton = (
+
unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
+ { _t('Unverify user') }
+
+ );
+ }
+
return (
-
{ _t("User Options") }
-
+
{ _t("Options") }
+
+ { directMessageButton }
{ readReceiptButton }
{ shareUserButton }
{ insertPillButton }
- { ignoreButton }
{ inviteUserButton }
+ { ignoreButton }
+ { unverifyButton }
);
@@ -337,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => {
return member.powerLevel < levelToSend;
};
-const useRoomPowerLevels = (room) => {
+const useRoomPowerLevels = (cli, room) => {
const [powerLevels, setPowerLevels] = useState({});
const update = useCallback(() => {
+ if (!room) {
+ return;
+ }
const event = room.currentState.getStateEvents("m.room.power_levels", "");
if (event) {
setPowerLevels(event.getContent());
@@ -352,7 +383,7 @@ const useRoomPowerLevels = (room) => {
};
}, [room]);
- useEventEmitter(room, "RoomState.events", update);
+ useEventEmitter(cli, "RoomState.members", update);
useEffect(() => {
update();
return () => {
@@ -399,7 +430,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
};
const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick");
- return
+ return
{ kickLabel }
;
});
@@ -472,7 +503,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
}
};
- return
+ return
{ _t("Remove recent messages") }
;
});
@@ -524,7 +555,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
label = _t("Unban");
}
- return
+ const classes = classNames("mx_UserInfo_field", {
+ mx_UserInfo_destructive: member.membership !== 'ban',
+ });
+
+ return
{ label }
;
});
@@ -581,21 +616,24 @@ const MuteToggleButton = withLegacyMatrixClient(
}
};
+ const classes = classNames("mx_UserInfo_field", {
+ mx_UserInfo_destructive: !isMuted,
+ });
+
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
- return
+ return
{ muteLabel }
;
},
);
const RoomAdminToolsContainer = withLegacyMatrixClient(
- ({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => {
+ ({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
let kickButton;
let banButton;
let muteButton;
let redactButton;
- const powerLevels = useRoomPowerLevels(room);
const editPowerLevel = (
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
@@ -705,7 +743,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient(
};
const kickButton = (
-
+
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
);
@@ -744,47 +782,17 @@ const useIsSynapseAdmin = (cli) => {
return isAdmin;
};
-// cli is injected by withLegacyMatrixClient
-const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
- // Load room if we are given a room id and memoize it
- const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
-
- // only display the devices list if our client supports E2E
- const _enableDevices = cli.isCryptoEnabled();
-
- // Load whether or not we are a Synapse Admin
- const isSynapseAdmin = useIsSynapseAdmin(cli);
-
- // Check whether the user is ignored
- const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
- // Recheck if the user or client changes
- useEffect(() => {
- setIsIgnored(cli.isUserIgnored(user.userId));
- }, [cli, user.userId]);
- // Recheck also if we receive new accountData m.ignored_user_list
- const accountDataHandler = useCallback((ev) => {
- if (ev.getType() === "m.ignored_user_list") {
- setIsIgnored(cli.isUserIgnored(user.userId));
- }
- }, [cli, user.userId]);
- useEventEmitter(cli, "accountData", accountDataHandler);
-
- // Count of how many operations are currently in progress, if > 0 then show a Spinner
- const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
- const startUpdating = useCallback(() => {
- setPendingUpdateCount(pendingUpdateCount + 1);
- }, [pendingUpdateCount]);
- const stopUpdating = useCallback(() => {
- setPendingUpdateCount(pendingUpdateCount - 1);
- }, [pendingUpdateCount]);
-
+function useRoomPermissions(cli, room, user) {
const [roomPermissions, setRoomPermissions] = useState({
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
modifyLevelMax: -1,
+ canEdit: false,
canInvite: false,
});
- const updateRoomPermissions = useCallback(async () => {
- if (!room) return;
+ const updateRoomPermissions = useCallback(() => {
+ if (!room) {
+ return;
+ }
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
@@ -811,20 +819,197 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
setRoomPermissions({
canInvite: me.powerLevel >= powerLevels.invite,
+ canEdit: modifyLevelMax >= 0,
modifyLevelMax,
});
}, [cli, user, room]);
- useEventEmitter(cli, "RoomState.events", updateRoomPermissions);
+ useEventEmitter(cli, "RoomState.members", updateRoomPermissions);
useEffect(() => {
updateRoomPermissions();
return () => {
setRoomPermissions({
maximalPowerLevel: -1,
+ canEdit: false,
canInvite: false,
});
};
}, [updateRoomPermissions]);
+ return roomPermissions;
+}
+
+const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => {
+ const [isEditing, setEditing] = useState(false);
+ if (room && user.roomId) { // is in room
+ if (isEditing) {
+ return ( setEditing(false)} />);
+ } else {
+ const IconButton = sdk.getComponent('elements.IconButton');
+ const powerLevelUsersDefault = powerLevels.users_default || 0;
+ const powerLevel = parseInt(user.powerLevel, 10);
+ const modifyButton = roomPermissions.canEdit ?
+ ( setEditing(true)} />) : null;
+ const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
+ const label = _t("%(role)s in %(roomName)s",
+ {role, roomName: room.name},
+ {strong: label => {label}},
+ );
+ return (
+
+
{label}{modifyButton}
+
+ );
+ }
+ } else {
+ return null;
+ }
+});
+
+const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
+ const [isDirty, setIsDirty] = useState(false);
+ const onPowerChange = useCallback((powerLevel) => {
+ setIsDirty(true);
+ setSelectedPowerLevel(parseInt(powerLevel, 10));
+ }, [setSelectedPowerLevel, setIsDirty]);
+
+ const changePowerLevel = useCallback(async () => {
+ const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
+ return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
+ function() {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ console.log("Power change success");
+ }, function(err) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Failed to change power level " + err);
+ Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
+ title: _t("Error"),
+ description: _t("Failed to change power level"),
+ });
+ },
+ );
+ };
+
+ try {
+ if (!isDirty) {
+ return;
+ }
+
+ setIsUpdating(true);
+
+ const powerLevel = selectedPowerLevel;
+
+ const roomId = user.roomId;
+ const target = user.userId;
+
+ const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
+ if (!powerLevelEvent) return;
+
+ if (!powerLevelEvent.getContent().users) {
+ _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
+ return;
+ }
+
+ const myUserId = cli.getUserId();
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+
+ // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
+ if (myUserId === target) {
+ try {
+ if (!(await _warnSelfDemote())) return;
+ } catch (e) {
+ console.error("Failed to warn about self demotion: ", e);
+ }
+ await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
+ return;
+ }
+
+ const myPower = powerLevelEvent.getContent().users[myUserId];
+ if (parseInt(myPower) === parseInt(powerLevel)) {
+ const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
+ title: _t("Warning!"),
+ description:
+
+ { _t("You will not be able to undo this change as you are promoting the user " +
+ "to have the same power level as yourself.") }
+ { _t("Are you sure?") }
+
,
+ button: _t("Continue"),
+ });
+
+ const [confirmed] = await finished;
+ if (confirmed) return;
+ }
+ await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
+ } finally {
+ onFinished();
+ }
+ }, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]);
+
+ const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
+ const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
+ const IconButton = sdk.getComponent('elements.IconButton');
+ const Spinner = sdk.getComponent("elements.Spinner");
+ const buttonOrSpinner = isUpdating ? :
+ ;
+
+ const PowerSelector = sdk.getComponent('elements.PowerSelector');
+ return (
+
+ );
+});
+
+// cli is injected by withLegacyMatrixClient
+const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
+ // Load room if we are given a room id and memoize it
+ const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
+
+ // only display the devices list if our client supports E2E
+ const _enableDevices = cli.isCryptoEnabled();
+
+ const powerLevels = useRoomPowerLevels(cli, room);
+ // Load whether or not we are a Synapse Admin
+ const isSynapseAdmin = useIsSynapseAdmin(cli);
+
+ // Check whether the user is ignored
+ const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
+ // Recheck if the user or client changes
+ useEffect(() => {
+ setIsIgnored(cli.isUserIgnored(user.userId));
+ }, [cli, user.userId]);
+ // Recheck also if we receive new accountData m.ignored_user_list
+ const accountDataHandler = useCallback((ev) => {
+ if (ev.getType() === "m.ignored_user_list") {
+ setIsIgnored(cli.isUserIgnored(user.userId));
+ }
+ }, [cli, user.userId]);
+ useEventEmitter(cli, "accountData", accountDataHandler);
+
+ // Count of how many operations are currently in progress, if > 0 then show a Spinner
+ const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
+ const startUpdating = useCallback(() => {
+ setPendingUpdateCount(pendingUpdateCount + 1);
+ }, [pendingUpdateCount]);
+ const stopUpdating = useCallback(() => {
+ setPendingUpdateCount(pendingUpdateCount - 1);
+ }, [pendingUpdateCount]);
+
+ const roomPermissions = useRoomPermissions(cli, room, user);
+
const onSynapseDeactivate = useCallback(async () => {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
@@ -842,80 +1027,25 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
const [accepted] = await finished;
if (!accepted) return;
try {
- cli.deactivateSynapseUser(user.userId);
+ await cli.deactivateSynapseUser(user.userId);
} catch (err) {
+ console.error("Failed to deactivate user");
+ console.error(err);
+
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
- Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, {
+ Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
title: _t('Failed to deactivate user'),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}, [cli, user.userId]);
- const onPowerChange = useCallback(async (powerLevel) => {
- const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
- startUpdating();
- cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
- function() {
- // NO-OP; rely on the m.room.member event coming down else we could
- // get out of sync if we force setState here!
- console.log("Power change success");
- }, function(err) {
- const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- console.error("Failed to change power level " + err);
- Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
- title: _t("Error"),
- description: _t("Failed to change power level"),
- });
- },
- ).finally(() => {
- stopUpdating();
- }).done();
- };
- const roomId = user.roomId;
- const target = user.userId;
-
- const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
- if (!powerLevelEvent) return;
-
- if (!powerLevelEvent.getContent().users) {
- _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
- return;
+ const onMemberAvatarKey = e => {
+ if (e.key === "Enter") {
+ onMemberAvatarClick();
}
-
- const myUserId = cli.getUserId();
- const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-
- // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
- if (myUserId === target) {
- try {
- if (!(await _warnSelfDemote())) return;
- _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
- } catch (e) {
- console.error("Failed to warn about self demotion: ", e);
- }
- return;
- }
-
- const myPower = powerLevelEvent.getContent().users[myUserId];
- if (parseInt(myPower) === parseInt(powerLevel)) {
- const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
- title: _t("Warning!"),
- description:
-
- { _t("You will not be able to undo this change as you are promoting the user " +
- "to have the same power level as yourself.") }
- { _t("Are you sure?") }
-
,
- button: _t("Continue"),
- });
-
- const [confirmed] = await finished;
- if (confirmed) return;
- }
- _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
- }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line
+ };
const onMemberAvatarClick = useCallback(() => {
const member = user;
@@ -935,17 +1065,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
let synapseDeactivateButton;
let spinner;
- let directChatsSection;
- if (user.userId !== cli.getUserId()) {
- directChatsSection = ;
- }
-
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
// someone does figure out how to bypass this check the worst that happens is an error.
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
synapseDeactivateButton = (
-
+
{_t("Deactivate user")}
);
@@ -955,6 +1080,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
if (room && user.roomId) {
adminToolsContainer = (
{ statusMessage };
}
- let memberDetails = null;
-
- if (room && user.roomId) { // is in room
- const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
- const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
-
- const PowerSelector = sdk.getComponent('elements.PowerSelector');
- memberDetails = ;
- }
-
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
- avatarElement =
-

+ avatarElement =
;
}
@@ -1058,6 +1171,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
title={_t('Close')} />;
}
+ const memberDetails =
;
+
+ const isRoomEncrypted = useIsEncrypted(cli, room);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
const [devices, setDevices] = useState(undefined);
// Download device lists
@@ -1082,14 +1201,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
setDevices(null);
}
}
-
- _downloadDeviceList();
+ if (isRoomEncrypted) {
+ _downloadDeviceList();
+ }
// Handle being unmounted
return () => {
cancelled = true;
};
- }, [cli, user.userId]);
+ }, [cli, user.userId, isRoomEncrypted]);
// Listen to changes
useEffect(() => {
@@ -1106,21 +1226,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
}
};
- cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
+ if (isRoomEncrypted) {
+ cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
+ }
// Handle being unmounted
return () => {
cancel = true;
- cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
+ if (isRoomEncrypted) {
+ cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
+ }
};
- }, [cli, user.userId]);
-
- let devicesSection;
- const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId);
- if (isRoomEncrypted) {
- devicesSection =
;
- } else {
- let text;
+ }, [cli, user.userId, isRoomEncrypted]);
+ let text;
+ if (!isRoomEncrypted) {
if (!_enableDevices) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
@@ -1128,22 +1247,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
} else {
// TODO what to render for GroupMember
}
-
- if (text) {
- devicesSection = (
-
-
{ _t("Trust & Devices") }
-
- { text }
-
-
- );
- }
+ } else {
+ text = _t("Messages in this room are end-to-end encrypted.");
}
+ const devicesSection = isRoomEncrypted ?
+ (
) : null;
+ const securitySection = (
+
+
{ _t("Security") }
+
{ text }
+
verifyDevice(user.userId, null)}>{_t("Verify")}
+ { devicesSection }
+
+ );
+
let e2eIcon;
if (isRoomEncrypted && devices) {
- e2eIcon =
;
+ e2eIcon =
;
}
return (
@@ -1153,16 +1274,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
-
-
+
+
{ e2eIcon }
{ displayName }
-
- { user.userId }
-
-
+
{ user.userId }
+
{presenceLabel}
{statusLabel}
@@ -1176,11 +1295,9 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
}
- { devicesSection }
-
- { directChatsSection }
-
+ { securitySection }
diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js
index aab6c04f53..952c49828b 100644
--- a/src/components/views/room_settings/ColorSettings.js
+++ b/src/components/views/room_settings/ColorSettings.js
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 2a0a7569fb..e53570dc5b 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -107,7 +107,9 @@ module.exports = createReactClass({
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
);
return widgets.map((ev) => {
- return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
+ return WidgetUtils.makeAppConfig(
+ ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(),
+ );
});
},
@@ -159,6 +161,7 @@ module.exports = createReactClass({
return ( {
- this.processQuery(query, selection).then(() => {
- deferred.resolve();
- });
- }, autocompleteDelay);
- return deferred.promise;
+ return new Promise((resolve) => {
+ this.debounceCompletionsRequest = setTimeout(() => {
+ resolve(this.processQuery(query, selection));
+ }, autocompleteDelay);
+ });
}
processQuery(query, selection) {
@@ -197,16 +195,16 @@ export default class Autocomplete extends React.Component {
}
forceComplete() {
- const done = Promise.defer();
- this.setState({
- forceComplete: true,
- hide: false,
- }, () => {
- this.complete(this.props.query, this.props.selection).then(() => {
- done.resolve(this.countCompletions());
+ return new Promise((resolve) => {
+ this.setState({
+ forceComplete: true,
+ hide: false,
+ }, () => {
+ this.complete(this.props.query, this.props.selection).then(() => {
+ resolve(this.countCompletions());
+ });
});
});
- return done.promise;
}
onCompletionClicked(selectionOffset: number): boolean {
diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js
index 54260e4ee2..d6baa30c8e 100644
--- a/src/components/views/rooms/E2EIcon.js
+++ b/src/components/views/rooms/E2EIcon.js
@@ -36,7 +36,13 @@ export default function(props) {
_t("All devices for this user are trusted") :
_t("All devices in this encrypted room are trusted");
}
- const icon = ();
+
+ let style = null;
+ if (props.size) {
+ style = {width: `${props.size}px`, height: `${props.size}px`};
+ }
+
+ const icon = ();
if (props.onClick) {
return ({ icon });
} else {
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 22f1f914b6..8b0e62d8f5 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -548,7 +548,7 @@ module.exports = createReactClass({
const SenderProfile = sdk.getComponent('messages.SenderProfile');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
- //console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
+ //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
const content = this.props.mxEvent.getContent();
const msgtype = content.msgtype;
@@ -606,8 +606,8 @@ module.exports = createReactClass({
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
- mx_EventTile_verified: this.state.verified === true,
- mx_EventTile_unverified: this.state.verified === false,
+ mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
+ mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
@@ -800,7 +800,7 @@ module.exports = createReactClass({
{ timestamp }
- { this._renderE2EPadlock() }
+ { !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
{ sender }
-
+
{ timestamp }
- { this._renderE2EPadlock() }
+ { !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
{
console.error("Failed to get URL preview: " + error);
- }).done();
+ });
},
componentDidMount: function() {
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 2ea6392e96..1a2c8e2212 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -248,7 +248,7 @@ module.exports = createReactClass({
return client.getStoredDevicesForUser(member.userId);
}).finally(function() {
self._cancelDeviceList = null;
- }).done(function(devices) {
+ }).then(function(devices) {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
@@ -550,7 +550,16 @@ module.exports = createReactClass({
danger: true,
onFinished: (accepted) => {
if (!accepted) return;
- this.context.matrixClient.deactivateSynapseUser(this.props.member.userId);
+ this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => {
+ console.error("Failed to deactivate user");
+ console.error(e);
+
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
+ title: _t('Failed to deactivate user'),
+ description: ((e && e.message) ? e.message : _t("Operation failed")),
+ });
+ });
},
});
},
@@ -572,7 +581,7 @@ module.exports = createReactClass({
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
- }).done();
+ });
},
onPowerChange: async function(powerLevel) {
@@ -629,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() {
@@ -689,7 +698,7 @@ module.exports = createReactClass({
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) {
- //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
+ //console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can;
}
const editPowerLevel = (
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 632ca53f82..128f9be964 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -25,7 +25,6 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
-import classNames from 'classnames';
import E2EIcon from './E2EIcon';
function ComposerAvatar(props) {
@@ -353,13 +352,9 @@ export default class MessageComposer extends React.Component {
);
}
- const wrapperClasses = classNames({
- mx_MessageComposer_wrapper: true,
- mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
- });
return (
-
+
{ controls }
diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js
index 6529b5b1da..a80602368f 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.js
+++ b/src/components/views/rooms/RoomBreadcrumbs.js
@@ -31,6 +31,9 @@ import {_t} from "../../../languageHandler";
const MAX_ROOMS = 20;
const MIN_ROOMS_BEFORE_ENABLED = 10;
+// The threshold time in milliseconds to wait for an autojoined room to show up.
+const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds
+
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
@@ -38,6 +41,10 @@ export default class RoomBreadcrumbs extends React.Component {
this.onAction = this.onAction.bind(this);
this._dispatcherRef = null;
+
+ // The room IDs we're waiting to come down the Room handler and when we
+ // started waiting for them. Used to track a room over an upgrade/autojoin.
+ this._waitingRoomQueue = [/* { roomId, addedTs } */];
}
componentWillMount() {
@@ -54,7 +61,7 @@ export default class RoomBreadcrumbs extends React.Component {
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
- MatrixClientPeg.get().on("Room", this.onRoomMembershipChanged);
+ MatrixClientPeg.get().on("Room", this.onRoom);
}
componentWillUnmount() {
@@ -68,7 +75,7 @@ export default class RoomBreadcrumbs extends React.Component {
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Event.decrypted", this.onEventDecrypted);
- client.removeListener("Room", this.onRoomMembershipChanged);
+ client.removeListener("Room", this.onRoom);
}
}
@@ -87,6 +94,12 @@ export default class RoomBreadcrumbs extends React.Component {
onAction(payload) {
switch (payload.action) {
case 'view_room':
+ if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) {
+ // Queue the room instead of pushing it immediately - we're probably just waiting
+ // for a join to complete (ie: joining the upgraded room).
+ this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()});
+ break;
+ }
this._appendRoomId(payload.room_id);
break;
@@ -153,7 +166,20 @@ export default class RoomBreadcrumbs extends React.Component {
if (!this.state.enabled && this._shouldEnable()) {
this.setState({enabled: true});
}
- }
+ };
+
+ onRoom = (room) => {
+ // Always check for membership changes when we see new rooms
+ this.onRoomMembershipChanged();
+
+ const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId);
+ if (!waitingRoom) return;
+ this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1);
+
+ const now = (new Date()).getTime();
+ if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
+ this._appendRoomId(room.roomId); // add the room we've been waiting for
+ };
_shouldEnable() {
const client = MatrixClientPeg.get();
diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js
index 4bb2f29e61..eb41f6729b 100644
--- a/src/components/views/rooms/SlateMessageComposer.js
+++ b/src/components/views/rooms/SlateMessageComposer.js
@@ -460,13 +460,9 @@ export default class SlateMessageComposer extends React.Component {
const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
- const wrapperClasses = classNames({
- mx_MessageComposer_wrapper: true,
- mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
- });
return (
-
+
{ controls }
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 28e51ed12e..7eabf27528 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -74,10 +74,10 @@ export default class Stickerpicker extends React.Component {
this.forceUpdate();
return this.scalarClient;
}).catch((e) => {
- this._imError(_td("Failed to connect to integrations server"), e);
+ this._imError(_td("Failed to connect to integration manager"), e);
});
} else {
- this._imError(_td("No integrations server is configured to manage stickers with"));
+ IntegrationManagers.sharedInstance().openNoManagerDialog();
}
}
@@ -287,12 +287,17 @@ export default class Stickerpicker extends React.Component {
return stickersContent;
}
- /**
+ // Dev note: this isn't jsdoc because it's angry.
+ /*
* Show the sticker picker overlay
* If no stickerpacks have been added, show a link to the integration manager add sticker packs page.
- * @param {Event} e Event that triggered the function
*/
_onShowStickersClick(e) {
+ if (!SettingsStore.getValue("integrationProvisioning")) {
+ // Intercept this case and spawn a warning.
+ return IntegrationManagers.sharedInstance().showDisabledDialog();
+ }
+
// XXX: Simplify by using a context menu that is positioned relative to the sticker picker button
const buttonRect = e.target.getBoundingClientRect();
@@ -346,7 +351,7 @@ export default class Stickerpicker extends React.Component {
}
/**
- * Launch the integrations manager on the stickers integration page
+ * Launch the integration manager on the stickers integration page
*/
_launchManageIntegrations() {
// TODO: Open the right integration manager for the widget
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 32521006c7..904b17b15f 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -112,7 +112,7 @@ module.exports = createReactClass({
}
});
- httpPromise.done(function() {
+ httpPromise.then(function() {
self.setState({
phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index a086efaa6d..a317c46cec 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -25,7 +25,6 @@ const Modal = require("../../../Modal");
const sdk = require("../../../index");
import dis from "../../../dispatcher";
-import Promise from 'bluebird';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
@@ -174,21 +173,16 @@ module.exports = createReactClass({
newPassword: "",
newPasswordConfirm: "",
});
- }).done();
+ });
},
_optionallySetEmail: function() {
- const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
- Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
+ const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'),
- onFinished: (confirmed) => {
- // ignore confirmed, setting an email is optional
- deferred.resolve(confirmed);
- },
});
- return deferred.promise;
+ return modal.finished.then(([confirmed]) => confirmed);
},
_onExportE2eKeysClicked: function() {
diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js
index 30f507ea18..cb5db10be4 100644
--- a/src/components/views/settings/DevicesPanel.js
+++ b/src/components/views/settings/DevicesPanel.js
@@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component {
}
_loadDevices() {
- MatrixClientPeg.get().getDevices().done(
+ MatrixClientPeg.get().getDevices().then(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});
diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationManager.js
similarity index 71%
rename from src/components/views/settings/IntegrationsManager.js
rename to src/components/views/settings/IntegrationManager.js
index d463b043d5..1ab17ca8a0 100644
--- a/src/components/views/settings/IntegrationsManager.js
+++ b/src/components/views/settings/IntegrationManager.js
@@ -21,12 +21,9 @@ import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
-export default class IntegrationsManager extends React.Component {
+export default class IntegrationManager extends React.Component {
static propTypes = {
- // false to display an error saying that there is no integrations manager configured
- configured: PropTypes.bool.isRequired,
-
- // false to display an error saying that we couldn't connect to the integrations manager
+ // false to display an error saying that we couldn't connect to the integration manager
connected: PropTypes.bool.isRequired,
// true to display a loading spinner
@@ -40,7 +37,6 @@ export default class IntegrationsManager extends React.Component {
};
static defaultProps = {
- configured: true,
connected: true,
loading: false,
};
@@ -70,20 +66,11 @@ export default class IntegrationsManager extends React.Component {
};
render() {
- if (!this.props.configured) {
- return (
-
-
{_t("No integrations server configured")}
-
{_t("This Riot instance does not have an integrations server configured.")}
-
- );
- }
-
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
-
-
{_t("Connecting to integrations server...")}
+
+
{_t("Connecting to integration manager...")}
);
@@ -91,9 +78,9 @@ export default class IntegrationsManager extends React.Component {
if (!this.props.connected) {
return (
-
-
{_t("Cannot connect to integrations server")}
-
{_t("The integrations server is offline or it cannot reach your homeserver.")}
+
+
{_t("Cannot connect to integration manager")}
+
{_t("The integration manager is offline or it cannot reach your homeserver.")}
);
}
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index e3b4cfe122..7345980bff 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
-import Promise from 'bluebird';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@@ -30,6 +29,7 @@ import {
} from '../../../notifications';
import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import AccessibleButton from "../elements/AccessibleButton";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
@@ -97,7 +97,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 +170,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 +274,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 +343,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 +398,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 +434,7 @@ module.exports = createReactClass({
}
}
- Promise.all(deferreds).done(function(resps) {
+ Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
@@ -650,11 +650,22 @@ module.exports = createReactClass({
externalContentRules: self.state.externalContentRules,
externalPushRules: self.state.externalPushRules,
});
- }).done();
+ });
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},
+ _onClearNotifications: function() {
+ const cli = MatrixClientPeg.get();
+
+ cli.getRooms().forEach(r => {
+ if (r.getUnreadNotificationCount() > 0) {
+ const events = r.getLiveTimeline().getEvents();
+ if (events.length) cli.sendReadReceipt(events.pop());
+ }
+ });
+ },
+
_updatePushRuleActions: function(rule, actions, enabled) {
const cli = MatrixClientPeg.get();
@@ -747,6 +758,13 @@ module.exports = createReactClass({
label={_t('Enable notifications for this account')}/>;
}
+ let clearNotificationsButton;
+ if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
+ clearNotificationsButton =
+ {_t("Clear notifications")}
+ ;
+ }
+
// When enabled, the master rule inhibits all existing rules
// So do not show all notification settings
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
@@ -757,6 +775,8 @@ module.exports = createReactClass({
{ _t('All notifications are currently disabled for all targets.') }
+
+ {clearNotificationsButton}
);
}
@@ -878,6 +898,7 @@ module.exports = createReactClass({
{ devicesSection }
+ { clearNotificationsButton }
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js
index 126cdc9557..a7a2e01c22 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.js
@@ -26,6 +26,7 @@ import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
+import {timeout} from "../../../utils/promise";
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@@ -245,14 +246,11 @@ export default class SetIdServer extends React.Component {
let threepids = [];
let currentServerReachable = true;
try {
- threepids = await Promise.race([
+ threepids = await timeout(
getThreepidsWithBindStatus(MatrixClientPeg.get()),
- new Promise((resolve, reject) => {
- setTimeout(() => {
- reject(new Error("Timeout attempting to reach identity server"));
- }, REACHABILITY_TIMEOUT);
- }),
- ]);
+ Promise.reject(new Error("Timeout attempting to reach identity server")),
+ REACHABILITY_TIMEOUT,
+ );
} catch (e) {
currentServerReachable = false;
console.warn(
diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js
index b1268c8048..e205f02e6c 100644
--- a/src/components/views/settings/SetIntegrationManager.js
+++ b/src/components/views/settings/SetIntegrationManager.js
@@ -16,13 +16,9 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../languageHandler";
-import sdk from '../../../index';
-import Field from "../elements/Field";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
-import MatrixClientPeg from "../../../MatrixClientPeg";
-import {SERVICE_TYPES} from "matrix-js-sdk";
-import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance";
-import Modal from "../../../Modal";
+import sdk from '../../../index';
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class SetIntegrationManager extends React.Component {
constructor() {
@@ -32,135 +28,23 @@ export default class SetIntegrationManager extends React.Component {
this.state = {
currentManager,
- url: "", // user-entered text
- error: null,
- busy: false,
- checking: false,
+ provisioningEnabled: SettingsStore.getValue("integrationProvisioning"),
};
}
- _onUrlChanged = (ev) => {
- const u = ev.target.value;
- this.setState({url: u});
- };
+ onProvisioningToggled = () => {
+ const current = this.state.provisioningEnabled;
+ SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
+ console.error("Error changing integration manager provisioning");
+ console.error(err);
- _getTooltip = () => {
- if (this.state.checking) {
- const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
- return
-
- { _t("Checking server") }
-
;
- } else if (this.state.error) {
- return
{this.state.error};
- } else {
- return null;
- }
- };
-
- _canChange = () => {
- return !!this.state.url && !this.state.busy;
- };
-
- _continueTerms = async (manager) => {
- try {
- await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
- this.setState({
- busy: false,
- error: null,
- currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
- url: "", // clear input
- });
- } catch (e) {
- console.error(e);
- this.setState({
- busy: false,
- error: _t("Failed to update integration manager"),
- });
- }
- };
-
- _setManager = async (ev) => {
- // Don't reload the page when the user hits enter in the form.
- ev.preventDefault();
- ev.stopPropagation();
-
- this.setState({busy: true, checking: true, error: null});
-
- let offline = false;
- let manager: IntegrationManagerInstance;
- try {
- manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
- offline = !manager; // no manager implies offline
- } catch (e) {
- console.error(e);
- offline = true; // probably a connection error
- }
- if (offline) {
- this.setState({
- busy: false,
- checking: false,
- error: _t("Integration manager offline or not accessible."),
- });
- return;
- }
-
- // Test the manager (causes terms of service prompt if agreement is needed)
- // We also cancel the tooltip at this point so it doesn't collide with the dialog.
- this.setState({checking: false});
- try {
- const client = manager.getScalarClient();
- await client.connect();
- } catch (e) {
- console.error(e);
- this.setState({
- busy: false,
- error: _t("Terms of service not accepted or the integration manager is invalid."),
- });
- return;
- }
-
- // Specifically request the terms of service to see if there are any.
- // The above won't trigger a terms of service check if there are no terms to
- // sign, so when there's no terms at all we need to ensure we tell the user.
- let hasTerms = true;
- try {
- const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl);
- hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0;
- } catch (e) {
- // Assume errors mean there are no terms. This could be a 404, 500, etc
- console.error(e);
- hasTerms = false;
- }
- if (!hasTerms) {
- this.setState({busy: false});
- const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
- Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
- title: _t("Integration manager has no terms of service"),
- description: (
-
-
- {_t("The integration manager you have chosen does not have any terms of service.")}
-
-
- {_t("Only continue if you trust the owner of the server.")}
-
-
- ),
- button: _t("Continue"),
- onFinished: async (confirmed) => {
- if (!confirmed) return;
- this._continueTerms(manager);
- },
- });
- return;
- }
-
- this._continueTerms(manager);
+ this.setState({provisioningEnabled: current});
+ });
+ this.setState({provisioningEnabled: !current});
};
render() {
- const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
+ const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch");
const currentManager = this.state.currentManager;
let managerName;
@@ -168,45 +52,32 @@ export default class SetIntegrationManager extends React.Component {
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
- "You are currently using
%(serverName)s to manage your bots, widgets, " +
+ "Use an Integration Manager
(%(serverName)s) to manage bots, widgets, " +
"and sticker packs.",
{serverName: currentManager.name},
{ b: sub =>
{sub} },
);
} else {
- bodyText = _t(
- "Add which integration manager you want to manage your bots, widgets, " +
- "and sticker packs.",
- );
+ bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
}
return (
-