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(
+ "