Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into rxl881/snapshot

This commit is contained in:
Richard Lewis 2017-12-15 10:18:56 +00:00
commit f410112983
32 changed files with 1483 additions and 322 deletions

View file

@ -18,6 +18,8 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk';
import React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import Notifier from '../../Notifier';
@ -38,7 +40,7 @@ import SettingsStore from "../../settings/SettingsStore";
*
* Components mounted below us can access the matrix client via the react context.
*/
export default React.createClass({
const LoggedInView = React.createClass({
displayName: 'LoggedInView',
propTypes: {
@ -344,3 +346,5 @@ export default React.createClass({
);
},
});
export default DragDropContext(HTML5Backend)(LoggedInView);

View file

@ -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";
@ -84,7 +83,7 @@ const ONBOARDING_FLOW_STARTERS = [
'view_create_group',
];
module.exports = React.createClass({
export default React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
VIEWS: VIEWS,
@ -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'});
});
},

View file

@ -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(
"<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.
_getContent: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
@ -263,28 +349,8 @@ module.exports = React.createClass({
);
}
if (this.props.unsentMessageError) {
return (
<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>
);
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);

View file

@ -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 'picture_snapshot':
this.uploadFile(payload.file);
@ -714,35 +705,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) {
@ -787,14 +749,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({
@ -938,11 +892,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");
@ -1574,12 +1524,9 @@ module.exports = React.createClass({
statusBar = <RoomStatusBar
room={this.state.room}
numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick}
onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline}

View file

@ -17,79 +17,17 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames';
import FilterStore from '../../stores/FilterStore';
import FlairStore from '../../stores/FlairStore';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index';
import dis from '../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
const TagTile = React.createClass({
displayName: 'TagTile',
propTypes: {
groupProfile: PropTypes.object,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
hover: false,
};
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.groupProfile.groupId,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onMouseOver: function() {
this.setState({hover: true});
},
onMouseOut: function() {
this.setState({hover: false});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.props.groupProfile || {};
const name = profile.name || profile.groupId;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.props.selected,
});
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
</div>
</AccessibleButton>;
},
});
export default React.createClass({
const TagPanel = React.createClass({
displayName: 'TagPanel',
contextTypes: {
@ -98,7 +36,17 @@ export default React.createClass({
getInitialState() {
return {
joinedGroupProfiles: [],
// A list of group profiles for tags that are group IDs. The intention in future
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
// For now, it suffices to maintain a list of ordered group profiles.
orderedGroupTagProfiles: [
// {
// groupId: '+awesome:foo.bar',{
// name: 'My Awesome Community',
// avatarUrl: 'mxc://...',
// shortDescription: 'Some description...',
// },
],
selectedTags: [],
};
},
@ -115,8 +63,23 @@ export default React.createClass({
selectedTags: FilterStore.getSelectedTags(),
});
});
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
}
this._fetchJoinedRooms();
const orderedTags = TagOrderStore.getOrderedTags() || [];
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
// XXX: One profile lookup failing will bring the whole lot down
Promise.all(orderedGroupTags.map(
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
)).then((orderedGroupTagProfiles) => {
if (this.unmounted) return;
this.setState({orderedGroupTagProfiles});
});
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
componentWillUnmount() {
@ -129,7 +92,7 @@ export default React.createClass({
_onGroupMyMembership() {
if (this.unmounted) return;
this._fetchJoinedRooms();
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
onClick() {
@ -141,27 +104,21 @@ export default React.createClass({
dis.dispatch({action: 'view_create_group'});
},
async _fetchJoinedRooms() {
const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups();
const joinedGroupIds = joinedGroupResponse.groups;
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map(
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
));
dis.dispatch({
action: 'all_tags',
tags: joinedGroupIds,
});
this.setState({joinedGroupProfiles});
onTagTileEndDrag() {
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
},
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => {
return <TagTile
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
return <DNDTagTile
key={groupProfile.groupId + '_' + index}
groupProfile={groupProfile}
selected={this.state.selectedTags.includes(groupProfile.groupId)}
onEndDrag={this.onTagTileEndDrag}
/>;
});
return <div className="mx_TagPanel" onClick={this.onClick}>
@ -174,3 +131,4 @@ export default React.createClass({
</div>;
},
});
export default TagPanel;