Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2017-12-08 16:33:30 +00:00
commit 07cd3d8b2e
10 changed files with 347 additions and 214 deletions

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //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) { function _setCallListeners(call) {
call.on("error", function(err) { call.on("error", function(err) {
console.error("Call error: %s", err); console.error("Call error: %s", err);
console.error(err.stack); console.error(err.stack);
call.hangup(); if (err.code === 'unknown_devices') {
_setCallState(undefined, call.roomId, "ended"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
});
call.on('send_event_error', function(err) { Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
if (err.name === "UnknownDeviceError") { title: _t('Call Failed'),
dis.dispatch({ description: _t(
action: 'unknown_device_error', "There are unknown devices in this room: "+
err: err, "if you proceed without verifying them, it will be "+
room: MatrixClientPeg.get().getRoom(call.roomId), "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 _onAction(payload) {
function placeCall(newCall) { function placeCall(newCall) {
_setCallListeners(newCall); _setCallListeners(newCall);
_setCallState(newCall, newCall.roomId, "ringback");
if (payload.type === 'voice') { if (payload.type === 'voice') {
newCall.placeVoiceCall(); newCall.placeVoiceCall();
} else if (payload.type === 'video') { } else if (payload.type === 'video') {

View file

@ -44,13 +44,6 @@ module.exports = {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('Resend got send failure: ' + err.name + '('+err+')'); 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({ dis.dispatch({
action: 'message_send_failed', action: 'message_send_failed',
@ -60,9 +53,5 @@ module.exports = {
}, },
removeFromQueue: function(event) { removeFromQueue: function(event) {
MatrixClientPeg.get().cancelPendingEvent(event); MatrixClientPeg.get().cancelPendingEvent(event);
dis.dispatch({
action: 'message_send_cancelled',
event: event,
});
}, },
}; };

View file

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

View file

@ -40,7 +40,6 @@ require('../../stores/LifecycleStore');
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import KeyRequestHandler from '../../KeyRequestHandler'; import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
@ -295,7 +294,6 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
UDEHandler.startListening();
this.focusComposer = false; this.focusComposer = false;
@ -361,7 +359,6 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
@ -1398,13 +1395,6 @@ module.exports = React.createClass({
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'}); dis.dispatch({action: 'message_sent'});
}, (err) => { }, (err) => {
if (err.name === 'UnknownDeviceError') {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: cli.getRoom(roomId),
});
}
dis.dispatch({action: 'message_send_failed'}); dis.dispatch({action: 'message_send_failed'});
}); });
}, },

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import Matrix from 'matrix-js-sdk';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import sdk from '../../index'; import sdk from '../../index';
import WhoIsTyping from '../../WhoIsTyping'; import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar'; import MemberAvatar from '../views/avatars/MemberAvatar';
import Resend from '../../Resend';
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2; 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({ module.exports = React.createClass({
displayName: 'RoomStatusBar', displayName: 'RoomStatusBar',
@ -35,9 +46,6 @@ module.exports = React.createClass({
// the number of messages which have arrived since we've been scrolled up // the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number, 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 // this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. // the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool, atEndOfLiveTimeline: React.PropTypes.bool,
@ -98,12 +106,14 @@ module.exports = React.createClass({
return { return {
syncState: MatrixClientPeg.get().getSyncState(), syncState: MatrixClientPeg.get().getSyncState(),
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
unsentMessages: getUnsentMessages(this.props.room),
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
this._checkSize(); this._checkSize();
}, },
@ -118,6 +128,7 @@ module.exports = React.createClass({
if (client) { if (client) {
client.removeListener("sync", this.onSyncStateChange); client.removeListener("sync", this.onSyncStateChange);
client.removeListener("RoomMember.typing", this.onRoomMemberTyping); 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 // Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function() { _checkSize: function() {
if (this.props.onVisible && this._getSize()) { if (this.props.onVisible && this._getSize()) {
@ -155,7 +186,7 @@ module.exports = React.createClass({
this.props.sentMessageAndIsAlone this.props.sentMessageAndIsAlone
) { ) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) { } else if (this.state.unsentMessages.length > 0) {
return STATUS_BAR_EXPANDED_LARGE; return STATUS_BAR_EXPANDED_LARGE;
} }
return STATUS_BAR_HIDDEN; return STATUS_BAR_HIDDEN;
@ -241,6 +272,61 @@ module.exports = React.createClass({
return avatars; 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(
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
{},
{
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
} 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("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
{},
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
}
return <div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ title }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{ content }
</div>
</div>;
},
// return suitable content for the main (text) part of the status bar. // return suitable content for the main (text) part of the status bar.
_getContent: function() { _getContent: function() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
@ -263,28 +349,8 @@ module.exports = React.createClass({
); );
} }
if (this.props.unsentMessageError) { if (this.state.unsentMessages.length > 0) {
return ( return this._getUnsentMessageContent();
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ this.props.unsentMessageError }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{
_t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
{},
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
},
) }
</div>
</div>
);
} }
// unread count trumps who is typing since the unread count is only // unread count trumps who is typing since the unread count is only
@ -342,7 +408,6 @@ module.exports = React.createClass({
return null; return null;
}, },
render: function() { render: function() {
const content = this._getContent(); const content = this._getContent();
const indicator = this._getIndicator(this.state.usersTyping.length > 0); const indicator = this._getIndicator(this.state.usersTyping.length > 0);

View file

@ -26,7 +26,6 @@ const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
const classNames = require("classnames"); const classNames = require("classnames");
const Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
const MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
@ -34,7 +33,6 @@ const ContentMessages = require("../../ContentMessages");
const Modal = require("../../Modal"); const Modal = require("../../Modal");
const sdk = require('../../index'); const sdk = require('../../index');
const CallHandler = require('../../CallHandler'); const CallHandler = require('../../CallHandler');
const Resend = require("../../Resend");
const dis = require("../../dispatcher"); const dis = require("../../dispatcher");
const Tinter = require("../../Tinter"); const Tinter = require("../../Tinter");
const rate_limited_func = require('../../ratelimitedfunc'); const rate_limited_func = require('../../ratelimitedfunc');
@ -110,7 +108,6 @@ module.exports = React.createClass({
draggingFile: false, draggingFile: false,
searching: false, searching: false,
searchResults: null, searchResults: null,
unsentMessageError: '',
callState: null, callState: null,
guestsCanJoin: false, guestsCanJoin: false,
canPeek: false, canPeek: false,
@ -202,7 +199,6 @@ module.exports = React.createClass({
if (initial) { if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId); newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
if (newState.room) { if (newState.room) {
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
newState.showApps = this._shouldShowApps(newState.room); newState.showApps = this._shouldShowApps(newState.room);
this._onRoomLoaded(newState.room); this._onRoomLoaded(newState.room);
} }
@ -462,11 +458,6 @@ module.exports = React.createClass({
case 'message_send_failed': case 'message_send_failed':
case 'message_sent': case 'message_sent':
this._checkIfAlone(this.state.room); this._checkIfAlone(this.state.room);
// no break; to intentionally fall through
case 'message_send_cancelled':
this.setState({
unsentMessageError: this._getUnsentMessageError(this.state.room),
});
break; break;
case 'notifier_enabled': case 'notifier_enabled':
case 'upload_failed': case 'upload_failed':
@ -711,35 +702,6 @@ module.exports = React.createClass({
this.setState({isAlone: joinedMembers.length === 1}); 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() { _updateConfCallNotification: function() {
const room = this.state.room; const room = this.state.room;
if (!room || !this.props.ConferenceHandler) { 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() { onInviteButtonClick: function() {
// call AddressPickerDialog // call AddressPickerDialog
dis.dispatch({ dis.dispatch({
@ -935,11 +889,7 @@ module.exports = React.createClass({
file, this.state.room.roomId, MatrixClientPeg.get(), file, this.state.room.roomId, MatrixClientPeg.get(),
).done(undefined, (error) => { ).done(undefined, (error) => {
if (error.name === "UnknownDeviceError") { if (error.name === "UnknownDeviceError") {
dis.dispatch({ // Let the staus bar handle this
action: 'unknown_device_error',
err: error,
room: this.state.room,
});
return; return;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -1571,12 +1521,9 @@ module.exports = React.createClass({
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline} atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone} sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall} hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick}
onInviteClick={this.onInviteButtonClick} onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick} onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline} onScrollToBottomClick={this.jumpToLiveTimeline}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
@ -22,6 +24,14 @@ import Resend from '../../../Resend';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
function markAllDevicesKnown(devices) {
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
}
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -38,10 +48,10 @@ function DeviceListEntry(props) {
} }
DeviceListEntry.propTypes = { DeviceListEntry.propTypes = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// deviceinfo // deviceinfo
device: React.PropTypes.object.isRequired, device: PropTypes.object.isRequired,
}; };
@ -61,10 +71,10 @@ function UserUnknownDeviceList(props) {
} }
UserUnknownDeviceList.propTypes = { UserUnknownDeviceList.propTypes = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// map from deviceid -> deviceinfo // map from deviceid -> deviceinfo
userDevices: React.PropTypes.object.isRequired, userDevices: PropTypes.object.isRequired,
}; };
@ -83,7 +93,7 @@ function UnknownDeviceList(props) {
UnknownDeviceList.propTypes = { UnknownDeviceList.propTypes = {
// map from userid -> deviceid -> deviceinfo // map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired, devices: PropTypes.object.isRequired,
}; };
@ -91,28 +101,63 @@ export default React.createClass({
displayName: 'UnknownDeviceDialog', displayName: 'UnknownDeviceDialog',
propTypes: { propTypes: {
room: React.PropTypes.object.isRequired, room: PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo // map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
devices: React.PropTypes.object.isRequired, devices: PropTypes.object,
onFinished: React.PropTypes.func.isRequired,
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() { componentWillMount: function() {
// Given we've now shown the user the unknown device, it is no longer MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
// 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);
});
});
// XXX: temporary logging to try to diagnose componentWillUnmount: function() {
// https://github.com/vector-im/riot-web/issues/3148 if (MatrixClientPeg.get()) {
console.log('Opening UnknownDeviceDialog'); 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() { render: function() {
if (this.props.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
let warning; let warning;
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) { if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = ( 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 = <button onClick={this._onSendAnywayClicked}>
{ this.props.sendAnywayLabel }
</button>;
} else {
sendButton = <button onClick={this._onSendClicked}>
{ this.props.sendLabel }
</button>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_UnknownDeviceDialog' <BaseDialog className='mx_UnknownDeviceDialog'
onFinished={() => { onFinished={this.props.onFinished}
// 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();
}}
title={_t('Room contains unknown devices')} title={_t('Room contains unknown devices')}
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
@ -154,21 +214,11 @@ export default React.createClass({
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
{sendButton}
<button className="mx_Dialog_primary" autoFocus={true} <button className="mx_Dialog_primary" autoFocus={true}
onClick={() => { onClick={this._onDismissClicked}
this.props.onFinished(); >
Resend.resendUnsentEvents(this.props.room); {_t("Dismiss")}
}}>
{ _t("Send anyway") }
</button>
<button className="mx_Dialog_primary" autoFocus={true}
onClick={() => {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log("UnknownDeviceDialog closed by OK");
this.props.onFinished();
}}>
OK
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -74,13 +74,6 @@ function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); 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({ dis.dispatch({
action: 'message_send_failed', action: 'message_send_failed',
}); });

104
src/cryptodevices.js Normal file
View file

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

View file

@ -2,6 +2,13 @@
"This email address is already in use": "This email address is already in use", "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", "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", "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", "Call Timeout": "Call Timeout",
"The remote side failed to pick up": "The remote side failed to pick up", "The remote side failed to pick up": "The remote side failed to pick up",
"Unable to capture screen": "Unable to capture screen", "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", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
"Failure to create room": "Failure to create room", "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.", "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", "Unnamed Room": "Unnamed Room",
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "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", "Not a valid Riot keyfile": "Not a valid Riot keyfile",
@ -695,7 +704,6 @@
"Room contains unknown devices": "Room contains unknown devices", "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.", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
"Unknown devices": "Unknown devices", "Unknown devices": "Unknown devices",
"Send anyway": "Send anyway",
"Private Chat": "Private Chat", "Private Chat": "Private Chat",
"Public Chat": "Public Chat", "Public Chat": "Public Chat",
"Custom": "Custom", "Custom": "Custom",
@ -760,17 +768,19 @@
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.", "To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.",
"You have no visible notifications": "You have no visible notifications", "You have no visible notifications": "You have no visible notifications",
"Scroll to bottom of page": "Scroll to bottom of page", "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",
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.": "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> 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.", "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.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
"%(count)s new messages|other": "%(count)s new messages", "%(count)s new messages|other": "%(count)s new messages",
"%(count)s new messages|one": "%(count)s new message", "%(count)s new messages|one": "%(count)s new message",
"Active call": "Active call", "Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?", "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"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 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?", "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", "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", "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big",
"Search failed": "Search failed", "Search failed": "Search failed",