diff --git a/src/CallHandler.js b/src/CallHandler.js index dd9d93709f..fd56d7f1b1 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -58,6 +59,7 @@ import sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; +import { showUnknownDeviceDialogForCalls } from './cryptodevices'; global.mxCalls = { //room_id: MatrixCall @@ -97,19 +99,54 @@ function pause(audioId) { } } +function _reAttemptCall(call) { + if (call.direction === 'outbound') { + dis.dispatch({ + action: 'place_call', + room_id: call.roomId, + type: call.type, + }); + } else { + call.answer(); + } +} + function _setCallListeners(call) { call.on("error", function(err) { console.error("Call error: %s", err); console.error(err.stack); - call.hangup(); - _setCallState(undefined, call.roomId, "ended"); - }); - call.on('send_event_error', function(err) { - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: MatrixClientPeg.get().getRoom(call.roomId), + if (err.code === 'unknown_devices') { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { + title: _t('Call Failed'), + description: _t( + "There are unknown devices in this room: "+ + "if you proceed without verifying them, it will be "+ + "possible for someone to eavesdrop on your call." + ), + button: _t('Review Devices'), + onFinished: function(confirmed) { + if (confirmed) { + const room = MatrixClientPeg.get().getRoom(call.roomId); + showUnknownDeviceDialogForCalls( + MatrixClientPeg.get(), + room, + () => { + _reAttemptCall(call); + }, + call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"), + call.direction === 'outbound' ? _t("Call") : _t("Answer"), + ); + } + }, + }); + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Call Failed'), + description: err.message, }); } }); @@ -179,7 +216,6 @@ function _setCallState(call, roomId, status) { function _onAction(payload) { function placeCall(newCall) { _setCallListeners(newCall); - _setCallState(newCall, newCall.roomId, "ringback"); if (payload.type === 'voice') { newCall.placeVoiceCall(); } else if (payload.type === 'video') { diff --git a/src/Resend.js b/src/Resend.js index 1fee5854ea..4eaee16d1b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -44,13 +44,6 @@ module.exports = { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('Resend got send failure: ' + err.name + '('+err+')'); - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: room, - }); - } dis.dispatch({ action: 'message_send_failed', @@ -60,9 +53,5 @@ module.exports = { }, removeFromQueue: function(event) { MatrixClientPeg.get().cancelPendingEvent(event); - dis.dispatch({ - action: 'message_send_cancelled', - event: event, - }); }, }; diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index c9d056f88e..3e775a94ab 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -76,6 +76,35 @@ class ScalarAuthClient { return defer.promise; } + getScalarPageTitle(url) { + const defer = Promise.defer(); + + let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; + scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); + scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); + request({ + method: 'GET', + uri: scalarPageLookupUrl, + json: true, + }, (err, response, body) => { + if (err) { + defer.reject(err); + } else if (response.statusCode / 100 !== 2) { + defer.reject({statusCode: response.statusCode}); + } else if (!body) { + defer.reject(new Error("Missing page title in response")); + } else { + let title = ""; + if (body.page_title_cache_item && body.page_title_cache_item.cached_title) { + title = body.page_title_cache_item.cached_title; + } + defer.resolve(title); + } + }); + + return defer.promise; + } + getScalarInterfaceUrlForRoom(roomId, screen, id) { let url = SdkConfig.get().integrations_ui_url; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 7698829647..7bde607451 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -366,6 +366,22 @@ function getWidgets(event, roomId) { sendResponse(event, widgetStateEvents); } +function getRoomEncState(event, roomId) { + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + const room = client.getRoom(roomId); + if (!room) { + sendError(event, _t('This room is not recognised.')); + return; + } + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); + + sendResponse(event, roomIsEncrypted); +} + function setPlumbingState(event, roomId, status) { if (typeof status !== 'string') { throw new Error('Plumbing state status should be a string'); @@ -593,6 +609,9 @@ const onMessage = function(event) { } else if (event.data.action === "get_widgets") { getWidgets(event, roomId); return; + } else if (event.data.action === "get_room_enc_state") { + getRoomEncState(event, roomId); + return; } else if (event.data.action === "can_send_event") { canSendEvent(event, roomId); return; diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js deleted file mode 100644 index e7d77b3b66..0000000000 --- a/src/UnknownDeviceErrorHandler.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import dis from './dispatcher'; -import sdk from './index'; -import Modal from './Modal'; - -let isDialogOpen = false; - -const onAction = function(payload) { - if (payload.action === 'unknown_device_error' && !isDialogOpen) { - const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); - isDialogOpen = true; - Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { - devices: payload.err.devices, - room: payload.room, - onFinished: (r) => { - isDialogOpen = false; - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('UnknownDeviceDialog closed with '+r); - }, - }, 'mx_Dialog_unknownDevice'); - } -}; - -let ref = null; - -export function startListening() { - ref = dis.register(onAction); -} - -export function stopListening() { - if (ref) { - dis.unregister(ref); - ref = null; - } -} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 74d5b91428..0f23413b5f 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -164,7 +164,7 @@ function stopListening() { function addEndpoint(widgetId, endpointUrl) { const u = URL.parse(endpointUrl); if (!u || !u.protocol || !u.host) { - console.warn("Invalid origin"); + console.warn("Invalid origin:", endpointUrl); return; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cd75ad8798..ba7251b603 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -40,7 +40,6 @@ require('../../stores/LifecycleStore'); import PageTypes from '../../PageTypes'; import createRoom from "../../createRoom"; -import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import KeyRequestHandler from '../../KeyRequestHandler'; import { _t, getCurrentLanguage } from '../../languageHandler'; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; @@ -295,7 +294,6 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - UDEHandler.startListening(); this.focusComposer = false; @@ -361,7 +359,6 @@ module.exports = React.createClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - UDEHandler.stopListening(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); }, @@ -1142,6 +1139,37 @@ module.exports = React.createClass({ room.setBlacklistUnverifiedDevices(blacklistEnabled); } }); + cli.on("crypto.warning", (type) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + switch (type) { + case 'CRYPTO_WARNING_ACCOUNT_MIGRATED': + Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { + title: _t('Cryptography data migrated'), + description: _t( + "A one-off migration of cryptography data has been performed. "+ + "End-to-end encryption will not work if you go back to an older "+ + "version of Riot. If you need to use end-to-end cryptography on "+ + "an older version, log out of Riot first. To retain message history, "+ + "export and re-import your keys.", + ), + }); + break; + case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': + Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { + title: _t('Old cryptography data detected'), + description: _t( + "Data from an older version of Riot has been detected. "+ + "This will have caused end-to-end cryptography to malfunction "+ + "in the older version. End-to-end encrypted messages exchanged "+ + "recently whilst using the older version may not be decryptable "+ + "in this version. This may also cause messages exchanged with this "+ + "version to fail. If you experience problems, log out and back in "+ + "again. To retain message history, export and re-import your keys.", + ), + }); + break; + } + }); }, /** @@ -1398,13 +1426,6 @@ module.exports = React.createClass({ cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { - if (err.name === 'UnknownDeviceError') { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: cli.getRoom(roomId), - }); - } dis.dispatch({action: 'message_send_failed'}); }); }, diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 03859f522e..77d506d9af 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +16,26 @@ limitations under the License. */ import React from 'react'; +import Matrix from 'matrix-js-sdk'; import { _t } from '../../languageHandler'; import sdk from '../../index'; import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; import MemberAvatar from '../views/avatars/MemberAvatar'; +import Resend from '../../Resend'; +import { showUnknownDeviceDialogForMessages } from '../../cryptodevices'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; +function getUnsentMessages(room) { + if (!room) { return []; } + return room.getPendingEvents().filter(function(ev) { + return ev.status === Matrix.EventStatus.NOT_SENT; + }); +}; + module.exports = React.createClass({ displayName: 'RoomStatusBar', @@ -35,9 +46,6 @@ module.exports = React.createClass({ // the number of messages which have arrived since we've been scrolled up numUnreadMessages: React.PropTypes.number, - // string to display when there are messages in the room which had errors on send - unsentMessageError: React.PropTypes.string, - // this is true if we are fully scrolled-down, and are looking at // the end of the live timeline. atEndOfLiveTimeline: React.PropTypes.bool, @@ -98,12 +106,14 @@ module.exports = React.createClass({ return { syncState: MatrixClientPeg.get().getSyncState(), usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), + unsentMessages: getUnsentMessages(this.props.room), }; }, componentWillMount: function() { MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); + MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); this._checkSize(); }, @@ -118,6 +128,7 @@ module.exports = React.createClass({ if (client) { client.removeListener("sync", this.onSyncStateChange); client.removeListener("RoomMember.typing", this.onRoomMemberTyping); + client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); } }, @@ -136,6 +147,26 @@ module.exports = React.createClass({ }); }, + _onResendAllClick: function() { + Resend.resendUnsentEvents(this.props.room); + }, + + _onCancelAllClick: function() { + Resend.cancelUnsentEvents(this.props.room); + }, + + _onShowDevicesClick: function() { + showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); + }, + + _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { + if (room.roomId !== this.props.room.roomId) return; + + this.setState({ + unsentMessages: getUnsentMessages(this.props.room), + }); + }, + // Check whether current size is greater than 0, if yes call props.onVisible _checkSize: function() { if (this.props.onVisible && this._getSize()) { @@ -155,7 +186,7 @@ module.exports = React.createClass({ this.props.sentMessageAndIsAlone ) { return STATUS_BAR_EXPANDED; - } else if (this.props.unsentMessageError) { + } else if (this.state.unsentMessages.length > 0) { return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; @@ -241,6 +272,61 @@ module.exports = React.createClass({ return avatars; }, + _getUnsentMessageContent: function() { + const unsentMessages = this.state.unsentMessages; + if (!unsentMessages.length) return null; + + let title; + let content; + + const hasUDE = unsentMessages.some((m) => { + return m.error && m.error.name === "UnknownDeviceError"; + }); + + if (hasUDE) { + title = _t("Message not sent due to unknown devices being present"); + content = _t( + "Show devices or cancel all.", + {}, + { + 'showDevicesText': (sub) => { sub }, + 'cancelText': (sub) => { sub }, + }, + ); + } else { + if ( + unsentMessages.length === 1 && + unsentMessages[0].error && + unsentMessages[0].error.data && + unsentMessages[0].error.data.error + ) { + title = unsentMessages[0].error.data.error; + } else { + title = _t("Some of your messages have not been sent."); + } + content = _t("Resend all or cancel all now. " + + "You can also select individual messages to resend or cancel.", + {}, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, + ); + } + + return
+ {_t("Warning")} +
+ { title } +
+
+ { content } +
+
; + }, + // return suitable content for the main (text) part of the status bar. _getContent: function() { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -263,28 +349,8 @@ module.exports = React.createClass({ ); } - if (this.props.unsentMessageError) { - return ( -
- /!\ -
- { this.props.unsentMessageError } -
-
- { - _t("Resend all or cancel all now. " + - "You can also select individual messages to resend or cancel.", - {}, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ) } -
-
- ); + if (this.state.unsentMessages.length > 0) { + return this._getUnsentMessageContent(); } // unread count trumps who is typing since the unread count is only @@ -342,7 +408,6 @@ module.exports = React.createClass({ return null; }, - render: function() { const content = this._getContent(); const indicator = this._getIndicator(this.state.usersTyping.length > 0); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1fda05fb76..138c110c4f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,7 +26,6 @@ const React = require("react"); const ReactDOM = require("react-dom"); import Promise from 'bluebird'; const classNames = require("classnames"); -const Matrix = require("matrix-js-sdk"); import { _t } from '../../languageHandler'; const MatrixClientPeg = require("../../MatrixClientPeg"); @@ -34,7 +33,6 @@ const ContentMessages = require("../../ContentMessages"); const Modal = require("../../Modal"); const sdk = require('../../index'); const CallHandler = require('../../CallHandler'); -const Resend = require("../../Resend"); const dis = require("../../dispatcher"); const Tinter = require("../../Tinter"); const rate_limited_func = require('../../ratelimitedfunc'); @@ -110,7 +108,6 @@ module.exports = React.createClass({ draggingFile: false, searching: false, searchResults: null, - unsentMessageError: '', callState: null, guestsCanJoin: false, canPeek: false, @@ -202,7 +199,6 @@ module.exports = React.createClass({ if (initial) { newState.room = MatrixClientPeg.get().getRoom(newState.roomId); if (newState.room) { - newState.unsentMessageError = this._getUnsentMessageError(newState.room); newState.showApps = this._shouldShowApps(newState.room); this._onRoomLoaded(newState.room); } @@ -462,11 +458,6 @@ module.exports = React.createClass({ case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); - // no break; to intentionally fall through - case 'message_send_cancelled': - this.setState({ - unsentMessageError: this._getUnsentMessageError(this.state.room), - }); break; case 'notifier_enabled': case 'upload_failed': @@ -711,35 +702,6 @@ module.exports = React.createClass({ this.setState({isAlone: joinedMembers.length === 1}); }, - _getUnsentMessageError: function(room) { - const unsentMessages = this._getUnsentMessages(room); - if (!unsentMessages.length) return ""; - - if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error && - unsentMessages[0].error.name !== "UnknownDeviceError" - ) { - return unsentMessages[0].error.data.error; - } - - for (const event of unsentMessages) { - if (!event.error || event.error.name !== "UnknownDeviceError") { - return _t("Some of your messages have not been sent."); - } - } - return _t("Message not sent due to unknown devices being present"); - }, - - _getUnsentMessages: function(room) { - if (!room) { return []; } - return room.getPendingEvents().filter(function(ev) { - return ev.status === Matrix.EventStatus.NOT_SENT; - }); - }, - _updateConfCallNotification: function() { const room = this.state.room; if (!room || !this.props.ConferenceHandler) { @@ -784,14 +746,6 @@ module.exports = React.createClass({ } }, - onResendAllClick: function() { - Resend.resendUnsentEvents(this.state.room); - }, - - onCancelAllClick: function() { - Resend.cancelUnsentEvents(this.state.room); - }, - onInviteButtonClick: function() { // call AddressPickerDialog dis.dispatch({ @@ -935,11 +889,7 @@ module.exports = React.createClass({ file, this.state.room.roomId, MatrixClientPeg.get(), ).done(undefined, (error) => { if (error.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: error, - room: this.state.room, - }); + // Let the staus bar handle this return; } const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -1571,12 +1521,9 @@ module.exports = React.createClass({ statusBar = { + Object.keys(devices[userId]).map((deviceId) => { + MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); + }); + }); +} + function DeviceListEntry(props) { const {userId, device} = props; @@ -38,10 +48,10 @@ function DeviceListEntry(props) { } DeviceListEntry.propTypes = { - userId: React.PropTypes.string.isRequired, + userId: PropTypes.string.isRequired, // deviceinfo - device: React.PropTypes.object.isRequired, + device: PropTypes.object.isRequired, }; @@ -61,10 +71,10 @@ function UserUnknownDeviceList(props) { } UserUnknownDeviceList.propTypes = { - userId: React.PropTypes.string.isRequired, + userId: PropTypes.string.isRequired, // map from deviceid -> deviceinfo - userDevices: React.PropTypes.object.isRequired, + userDevices: PropTypes.object.isRequired, }; @@ -83,7 +93,7 @@ function UnknownDeviceList(props) { UnknownDeviceList.propTypes = { // map from userid -> deviceid -> deviceinfo - devices: React.PropTypes.object.isRequired, + devices: PropTypes.object.isRequired, }; @@ -91,28 +101,63 @@ export default React.createClass({ displayName: 'UnknownDeviceDialog', propTypes: { - room: React.PropTypes.object.isRequired, + room: PropTypes.object.isRequired, - // map from userid -> deviceid -> deviceinfo - devices: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func.isRequired, + // map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded + devices: PropTypes.object, + + onFinished: PropTypes.func.isRequired, + + // Label for the button that marks all devices known and tries the send again + sendAnywayLabel: PropTypes.string.isRequired, + + // Label for the button that to send the event if you've verified all devices + sendLabel: PropTypes.string.isRequired, + + // function to retry the request once all devices are verified / known + onSend: PropTypes.func.isRequired, }, - componentDidMount: function() { - // Given we've now shown the user the unknown device, it is no longer - // unknown to them. Therefore mark it as 'known'. - Object.keys(this.props.devices).forEach((userId) => { - Object.keys(this.props.devices[userId]).map((deviceId) => { - MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); - }); - }); + componentWillMount: function() { + MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged); + }, - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('Opening UnknownDeviceDialog'); + componentWillUnmount: function() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged); + } + }, + + _onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) { + if (this.props.devices[userId] && this.props.devices[userId][deviceId]) { + // XXX: Mutating props :/ + this.props.devices[userId][deviceId] = deviceInfo; + this.forceUpdate(); + } + }, + + _onDismissClicked: function() { + this.props.onFinished(); + }, + + _onSendAnywayClicked: function() { + markAllDevicesKnown(this.props.devices); + + this.props.onFinished(); + this.props.onSend(); + }, + + _onSendClicked: function() { + this.props.onFinished(); + this.props.onSend(); }, render: function() { + if (this.props.devices === null) { + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + let warning; if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) { warning = ( @@ -133,15 +178,30 @@ export default React.createClass({ ); } + let haveUnknownDevices = false; + Object.keys(this.props.devices).forEach((userId) => { + Object.keys(this.props.devices[userId]).map((deviceId) => { + const device = this.props.devices[userId][deviceId]; + if (device.isUnverified() && !device.isKnown()) { + haveUnknownDevices = true; + } + }); + }); + let sendButton; + if (haveUnknownDevices) { + sendButton = ; + } else { + sendButton = ; + } + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log("UnknownDeviceDialog closed by escape"); - this.props.onFinished(); - }} + onFinished={this.props.onFinished} title={_t('Room contains unknown devices')} contentId='mx_Dialog_content' > @@ -155,21 +215,11 @@ export default React.createClass({
+ {sendButton} -
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 1dbb7af586..0d67b4c814 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -52,11 +52,13 @@ export default React.createClass({ userId: React.PropTypes.string.isRequired, // UserId of the entity that added / modified the widget creatorUserId: React.PropTypes.string, + waitForIframeLoad: React.PropTypes.bool, }, getDefaultProps() { return { url: "", + waitForIframeLoad: true, }; }, @@ -71,7 +73,7 @@ export default React.createClass({ const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); return { initialising: true, // True while we are mangling the widget URL - loading: true, // True while the iframe content is loading + loading: this.props.waitForIframeLoad, // True while the iframe content is loading widgetUrl: this._addWurlParams(newProps.url), widgetPermissionId: widgetPermissionId, // Assume that widget has permission to load if we are the user who @@ -79,7 +81,7 @@ export default React.createClass({ hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, error: null, deleting: false, - widgetPageTitle: null, + widgetPageTitle: newProps.widgetPageTitle, }; }, @@ -196,6 +198,11 @@ export default React.createClass({ widgetUrl: u.format(), initialising: false, }); + + // Fetch page title from remote content if not already set + if (!this.state.widgetPageTitle && params.url) { + this._fetchWidgetTitle(params.url); + } }, (err) => { console.error("Failed to get scalar_token", err); this.setState({ @@ -215,10 +222,14 @@ export default React.createClass({ if (nextProps.url !== this.props.url) { this._getNewState(nextProps); this.setScalarToken(); - } else if (nextProps.show && !this.props.show) { + } else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) { this.setState({ loading: true, }); + } else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) { + this.setState({ + widgetPageTitle: nextProps.widgetPageTitle, + }); } }, @@ -299,12 +310,16 @@ export default React.createClass({ /** * Set remote content title on AppTile - * @param {string} title Title string to set on the AppTile + * @param {string} url Url to check for title */ - _updateWidgetTitle(title) { - if (title) { - this.setState({widgetPageTitle: null}); - } + _fetchWidgetTitle(url) { + this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => { + if (widgetPageTitle) { + this.setState({widgetPageTitle: widgetPageTitle}); + } + }, (err) =>{ + console.error("Failed to get page title", err); + }); }, // Widget labels to render, depending upon user permissions @@ -430,13 +445,24 @@ export default React.createClass({ deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; } + const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg'); + return (
- { this.formatAppTileName() } - { this.state.widgetPageTitle && ( -  - { this.state.widgetPageTitle } - ) } + + + { this.formatAppTileName() } + { this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && ( +  - { this.state.widgetPageTitle } + ) } + { /* Edit widget */ } { showEditButton && - { expanded ? 'collapse' : 'expand' } + { expanded ? _t('collapse') : _t('expand') }
); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index cb897c9daf..f1c5c0000c 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -253,7 +253,7 @@ module.exports = React.createClass({ return (
-

Addresses

+

{ _t('Addresses') }

{ _t('The main address for this room is') }: { canonical_alias_section }
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 423f345b1d..78677e138b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -133,14 +133,17 @@ module.exports = React.createClass({ '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', }; + app.id = appId; + app.name = app.name || app.type; + if (app.data) { Object.keys(app.data).forEach((key) => { params['$' + key] = app.data[key]; }); + + app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true); } - app.id = appId; - app.name = app.name || app.type; app.url = this.encodeUri(app.url, params); app.creatorUserId = (sender && sender.userId) ? sender.userId : null; @@ -224,6 +227,8 @@ module.exports = React.createClass({ userId={this.props.userId} show={this.props.showApps} creatorUserId={app.creatorUserId} + widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''} + waitForIframeLoad={app.waitForIframeLoad} />); }); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index cd30f20645..1ffbfafbc5 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -74,13 +74,6 @@ function onSendMessageFailed(err, room) { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: room, - }); - } dis.dispatch({ action: 'message_send_failed', }); diff --git a/src/cryptodevices.js b/src/cryptodevices.js new file mode 100644 index 0000000000..c93e04253f --- /dev/null +++ b/src/cryptodevices.js @@ -0,0 +1,104 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Resend from './Resend'; +import sdk from './index'; +import Modal from './Modal'; +import { _t } from './languageHandler'; + +/** + * Gets all crypto devices in a room that are marked neither known + * nor verified. + * + * @param {MatrixClient} matrixClient A MatrixClient + * @param {Room} room js-sdk room object representing the room + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto~DeviceInfo|DeviceInfo}. + */ +export function getUnknownDevicesForRoom(matrixClient, room) { + const roomMembers = room.getJoinedMembers().map((m) => { + return m.userId; + }); + return matrixClient.downloadKeys(roomMembers, false).then((devices) => { + const unknownDevices = {}; + // This is all devices in this room, so find the unknown ones. + Object.keys(devices).forEach((userId) => { + Object.keys(devices[userId]).map((deviceId) => { + const device = devices[userId][deviceId]; + + if (device.isUnverified() && !device.isKnown()) { + if (unknownDevices[userId] === undefined) { + unknownDevices[userId] = {}; + } + unknownDevices[userId][deviceId] = device; + } + }); + }); + return unknownDevices; + }); +} + +/** + * Show the UnknownDeviceDialog for a given room. The dialog will inform the user + * that messages they sent to this room have not been sent due to unknown devices + * being present. + * + * @param {MatrixClient} matrixClient A MatrixClient + * @param {Room} room js-sdk room object representing the room + */ +export function showUnknownDeviceDialogForMessages(matrixClient, room) { + getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => { + const onSendClicked = () => { + Resend.resendUnsentEvents(room); + }; + + const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); + Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, { + room: room, + devices: unknownDevices, + sendAnywayLabel: _t("Send anyway"), + sendLabel: _t("Send"), + onSend: onSendClicked, + }, 'mx_Dialog_unknownDevice'); + }); +} + +/** + * Show the UnknownDeviceDialog for a given room. The dialog will inform the user + * that a call they tried to place or answer in the room couldn't be placed or + * answered due to unknown devices being present. + * + * @param {MatrixClient} matrixClient A MatrixClient + * @param {Room} room js-sdk room object representing the room + * @param {func} sendAnyway Function called when the 'call anyway' or 'call' + * button is pressed. This should attempt to place or answer the call again. + * @param {string} sendAnywayLabel Label for the button displayed to retry the call + * when unknown devices are still present (eg. "Call Anyway") + * @param {string} sendLabel Label for the button displayed to retry the call + * after all devices have been verified (eg. "Call") + */ +export function showUnknownDeviceDialogForCalls(matrixClient, room, sendAnyway, sendAnywayLabel, sendLabel) { + getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => { + const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); + Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, { + room: room, + devices: unknownDevices, + sendAnywayLabel: sendAnywayLabel, + sendLabel: sendLabel, + onSend: sendAnyway, + }, 'mx_Dialog_unknownDevice'); + }); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00d260c88d..72517a6de3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2,6 +2,13 @@ "This email address is already in use": "This email address is already in use", "This phone number is already in use": "This phone number is already in use", "Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email", + "Call Failed": "Call Failed", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.", + "Review Devices": "Review Devices", + "Call Anyway": "Call Anyway", + "Answer Anyway": "Answer Anyway", + "Call": "Call", + "Answer": "Answer", "Call Timeout": "Call Timeout", "The remote side failed to pick up": "The remote side failed to pick up", "Unable to capture screen": "Unable to capture screen", @@ -156,6 +163,8 @@ "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "Failure to create room": "Failure to create room", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Send anyway": "Send anyway", + "Send": "Send", "Unnamed Room": "Unnamed Room", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", @@ -448,6 +457,7 @@ "not specified": "not specified", "not set": "not set", "Remote addresses for this room:": "Remote addresses for this room:", + "Addresses": "Addresses", "The main address for this room is": "The main address for this room is", "Local addresses for this room:": "Local addresses for this room:", "This room has no local addresses": "This room has no local addresses", @@ -613,6 +623,8 @@ "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", + "collapse": "collapse", + "expand": "expand", "Custom of %(powerLevel)s": "Custom of %(powerLevel)s", "Custom level": "Custom level", "Room directory": "Room directory", @@ -693,7 +705,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Send anyway": "Send anyway", "Private Chat": "Private Chat", "Public Chat": "Public Chat", "Custom": "Custom", @@ -749,6 +760,10 @@ "Failed to leave room": "Failed to leave room", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", + "Cryptography data migrated": "Cryptography data migrated", + "A one-off migration of cryptography data has been performed. End-to-end encryption will not work if you go back to an older version of Riot. If you need to use end-to-end cryptography on an older version, log out of Riot first. To retain message history, export and re-import your keys.": "A one-off migration of cryptography data has been performed. End-to-end encryption will not work if you go back to an older version of Riot. If you need to use end-to-end cryptography on an older version, log out of Riot first. To retain message history, export and re-import your keys.", + "Old cryptography data detected": "Old cryptography data detected", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Logout": "Logout", "Your Communities": "Your Communities", "Error whilst fetching joined communities": "Error whilst fetching joined communities", @@ -758,17 +773,19 @@ "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.", "You have no visible notifications": "You have no visible notifications", "Scroll to bottom of page": "Scroll to bottom of page", + "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", + "Show devices or cancel all.": "Show devices or cancel all.", + "Some of your messages have not been sent.": "Some of your messages have not been sent.", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", + "Warning": "Warning", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", - "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", "%(count)s new messages|other": "%(count)s new messages", "%(count)s new messages|one": "%(count)s new message", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", - "Some of your messages have not been sent.": "Some of your messages have not been sent.", - "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Failed to upload file": "Failed to upload file", "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big", "Search failed": "Search failed",