Merge branch 'develop' into luke/fix-room-list-group-store-leak
This commit is contained in:
commit
71740cc176
32 changed files with 1488 additions and 323 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
@ -1068,10 +1065,10 @@ module.exports = React.createClass({
|
|||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self.refs.loggedInView) {
|
||||
if (!self._loggedInView) {
|
||||
return true;
|
||||
}
|
||||
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||
return self._loggedInView.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
|
@ -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'});
|
||||
});
|
||||
},
|
||||
|
@ -1466,6 +1487,10 @@ module.exports = React.createClass({
|
|||
return this.props.makeRegistrationUrl(params);
|
||||
},
|
||||
|
||||
_collectLoggedInView: function(ref) {
|
||||
this._loggedInView = ref;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
|
@ -1498,7 +1523,7 @@ module.exports = React.createClass({
|
|||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onRegistered={this.onRegistered}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = <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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations 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,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
@ -22,6 +24,14 @@ import Resend from '../../../Resend';
|
|||
import { _t } from '../../../languageHandler';
|
||||
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) {
|
||||
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 <Spinner />;
|
||||
}
|
||||
|
||||
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 = <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');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
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();
|
||||
}}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Room contains unknown devices')}
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
|
@ -154,21 +214,11 @@ export default React.createClass({
|
|||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{sendButton}
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={() => {
|
||||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
{ _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
|
||||
onClick={this._onDismissClicked}
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -22,6 +22,7 @@ import React from 'react';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -51,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,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -70,17 +73,46 @@ 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
|
||||
widgetUrl: newProps.url,
|
||||
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
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
const u = url.parse(urlString);
|
||||
if (!u) {
|
||||
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||
return url;
|
||||
}
|
||||
|
||||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL
|
||||
params.parentUrl = window.location.href;
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this._getNewState(this.props);
|
||||
},
|
||||
|
@ -122,6 +154,8 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount() {
|
||||
WidgetMessaging.startListening();
|
||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
this.setScalarToken();
|
||||
},
|
||||
|
@ -137,7 +171,7 @@ export default React.createClass({
|
|||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this.props.url,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
|
@ -150,7 +184,7 @@ export default React.createClass({
|
|||
this._scalarClient.getScalarToken().done((token) => {
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this.props.url);
|
||||
const u = url.parse(this._addWurlParams(this.props.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
|
@ -164,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({
|
||||
|
@ -174,6 +213,8 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetMessaging.stopListening();
|
||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
},
|
||||
|
||||
|
@ -181,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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -256,10 +301,27 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
this.setState({loading: false});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_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
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
_deleteWidgetLabel() {
|
||||
|
@ -305,6 +367,15 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
},
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -320,11 +391,6 @@ export default React.createClass({
|
|||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
|
@ -347,7 +413,7 @@ export default React.createClass({
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
ref="appFrame"
|
||||
src={safeWidgetUrl}
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded}
|
||||
|
@ -379,10 +445,24 @@ export default React.createClass({
|
|||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
<span className="mx_AppTileMenuBarTitle">
|
||||
<TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||
<span> - { this.state.widgetPageTitle }</span>
|
||||
) }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
|
|
85
src/components/views/elements/DNDTagTile.js
Normal file
85
src/components/views/elements/DNDTagTile.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* eslint new-cap: "off" */
|
||||
/*
|
||||
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 { DragSource, DropTarget } from 'react-dnd';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import dis from '../../../dispatcher';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
const tagTileSource = {
|
||||
canDrag: function(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
beginDrag: function(props) {
|
||||
// Return the data describing the dragged item
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor, component) {
|
||||
const dropResult = monitor.getDropResult();
|
||||
if (!monitor.didDrop() || !dropResult) {
|
||||
return;
|
||||
}
|
||||
props.onEndDrag();
|
||||
},
|
||||
};
|
||||
|
||||
const tagTileTarget = {
|
||||
canDrop(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
hover(props, monitor, component) {
|
||||
if (!monitor.canDrop()) return;
|
||||
const draggedY = monitor.getClientOffset().y;
|
||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||
const targetY = (top + bottom) / 2;
|
||||
dis.dispatch({
|
||||
action: 'order_tag',
|
||||
tag: monitor.getItem().tag,
|
||||
targetTag: props.groupProfile.groupId,
|
||||
// Note: we indicate that the tag should be after the target when
|
||||
// it's being dragged over the top half of the target.
|
||||
after: draggedY < targetY,
|
||||
});
|
||||
},
|
||||
|
||||
drop(props) {
|
||||
// Return the data to be returned by getDropResult
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default
|
||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
}))((props) => {
|
||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||
return connectDropTarget(connectDragSource(
|
||||
<div>
|
||||
<TagTile {...otherProps} />
|
||||
</div>,
|
||||
));
|
||||
}));
|
|
@ -478,7 +478,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
const toggleButton = (
|
||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||
{ expanded ? 'collapse' : 'expand' }
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
88
src/components/views/elements/TagTile.js
Normal file
88
src/components/views/elements/TagTile.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
|
||||
export default 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>;
|
||||
},
|
||||
});
|
|
@ -253,7 +253,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>Addresses</h3>
|
||||
<h3>{ _t('Addresses') }</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
/>);
|
||||
});
|
||||
|
||||
|
|
|
@ -196,6 +196,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
*/
|
||||
_onDecrypted: function() {
|
||||
// we need to re-verify the sending device.
|
||||
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption
|
||||
// has caused a change in size of the event tile)
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
@ -214,6 +216,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
|
||||
this.setState({
|
||||
verified: verified,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onWidgetLoad();
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue