Merge up from develop

This commit is contained in:
wmwragg 2016-09-13 12:37:52 +01:00
commit 524eeaa315
27 changed files with 780 additions and 171 deletions

View file

@ -188,7 +188,10 @@ module.exports = React.createClass({
for (let i = 0; i < dmRooms.length; i++) {
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
if (room) {
return room;
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
return room;
}
}
}
}
@ -221,6 +224,17 @@ module.exports = React.createClass({
})
.done();
}
// // Start the chat
// createRoom({dmUserId: addr})
// .catch(function(err) {
// var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// Modal.createDialog(ErrorDialog, {
// title: "Failure to invite user",
// description: err.toString()
// });
// return null;
// })
// .done();
// Close - this will happen before the above, as that is async
this.props.onFinished(true, addr);

View file

@ -0,0 +1,123 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
var React = require("react");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'EncryptedEventDialog',
propTypes: {
onFinished: React.PropTypes.func,
},
componentWillMount: function() {
var client = MatrixClientPeg.get();
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
refreshDevice: function() {
// XXX: gutwrench - is there any reason not to expose this on MatrixClient itself?
return MatrixClientPeg.get()._crypto.getDeviceByIdentityKey(
this.props.event.getSender(),
this.props.event.getWireContent().algorithm,
this.props.event.getWireContent().sender_key
);
},
getInitialState: function() {
return { device: this.refreshDevice() };
},
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.event.getSender()) {
this.setState({ device: this.refreshDevice() });
}
},
render: function() {
var event = this.props.event;
var device = this.state.device;
var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
return (
<div className="mx_EncryptedEventDialog">
<div className="mx_Dialog_title">
End-to-end encryption information
</div>
<div className="mx_Dialog_content">
<table>
<tbody>
<tr>
<td>Sent by</td>
<td>{ event.getSender() }</td>
</tr>
<tr>
<td>Sender device name</td>
<td>{ device.getDisplayName() }</td>
</tr>
<tr>
<td>Sender device ID</td>
<td>{ device.deviceId }</td>
</tr>
<tr>
<td>Sender device verification:</td>
<td>{ MatrixClientPeg.get().isEventSenderVerified(event) ? "verified" : <b>NOT verified</b> }</td>
</tr>
<tr>
<td>Sender device ed25519 fingerprint</td>
<td>{ device.getFingerprint() }</td>
</tr>
<tr>
<td>Sender device curve25519 identity key</td>
<td>{ event.getWireContent().sender_key }</td>
</tr>
<tr>
<td>Algorithm</td>
<td>{ event.getWireContent().algorithm }</td>
</tr>
{
event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr>
<td>Decryption error</td>
<td>{ event.getContent().body }</td>
</tr>
) : ''
}
</tbody>
</table>
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
OK
</button>
<MemberDeviceInfo ref="memberDeviceInfo" hideInfo={true} device={ this.state.device } userId={ this.props.event.getSender() }/>
</div>
</div>
);
}
});

View file

@ -18,6 +18,11 @@ var dis = require("../../../dispatcher");
module.exports = React.createClass({
displayName: 'LogoutPrompt',
propTypes: {
onFinished: React.PropTypes.func,
},
logOut: function() {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {

View file

@ -57,18 +57,34 @@ module.exports = React.createClass({
var TintableSvg = sdk.getComponent("elements.TintableSvg");
if (httpUrl) {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {text}
</a>
</div>
</span>
);
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a className="mx_ImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{ content.body && content.body.length > 0 ? content.body : "Attachment" }
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
</div>
</div>
</span>
);
}
else {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {text}
</a>
</div>
</span>
);
}
} else {
var extra = text ? ': '+text : '';
var extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody">
Invalid file{extra}
</span>

View file

@ -123,6 +123,30 @@ module.exports = React.createClass({
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
var download;
if (this.props.tileShape === "file_grid") {
download = (
<div className="mx_MImageBody_download">
<a className="mx_MImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{content.body}
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
</div>
</div>
);
}
else {
download = (
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</div>
);
}
var thumbUrl = this._getThumbUrl();
if (thumbUrl) {
return (
@ -133,12 +157,7 @@ module.exports = React.createClass({
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</div>
{ download }
</span>
);
} else if (content.body) {

View file

@ -69,12 +69,38 @@ module.exports = React.createClass({
}
}
var download;
if (this.props.tileShape === "file_grid") {
download = (
<div className="mx_MImageBody_download">
<a className="mx_MImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{content.body}
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
</div>
</div>
);
}
else {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
download = (
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</div>
);
}
return (
<span className="mx_MVideoBody">
<video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
controls preload={preload} autoPlay={false}
height={height} width={width} poster={poster}>
</video>
{ download }
</span>
);
},

View file

@ -37,6 +37,9 @@ module.exports = React.createClass({
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
/* the shsape of the tile, used */
tileShape: React.PropTypes.string,
},
getEventTileOps: function() {
@ -69,6 +72,7 @@ module.exports = React.createClass({
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />;
},
});

View file

@ -93,8 +93,9 @@ module.exports = React.createClass({
}
else {
joinText = (<span>
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}} href="#">voice</a>&nbsp;
or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }} href="#">video</a>.
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}}
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }}
href="#">video</a>.
</span>);
}

View file

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react');
var classNames = require("classnames");
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg')
@ -128,6 +129,15 @@ module.exports = React.createClass({
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
/* the shape of the tile. by default, the layout is intended for the
* normal room timeline. alternative values are: "file_list", "file_grid"
* and "notif". This could be done by CSS, but it'd be horribly inefficient.
* It could also be done by subclassing EventTile, but that'd be quite
* boiilerplatey. So just make the necessary render decisions conditional
* for now.
*/
tileShape: React.PropTypes.string,
},
getInitialState: function() {
@ -239,8 +249,7 @@ module.exports = React.createClass({
if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients
if (this.props.mxEvent.sender &&
this.props.mxEvent.sender.userId === MatrixClientPeg.get().credentials.userId)
if (this.props.mxEvent.getSender() === MatrixClientPeg.get().credentials.userId)
{
return false;
}
@ -353,6 +362,15 @@ module.exports = React.createClass({
});
},
onCryptoClicked: function(e) {
var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog");
var event = this.props.mxEvent;
Modal.createDialog(EncryptedEventDialog, {
event: event,
});
},
render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -366,7 +384,7 @@ module.exports = React.createClass({
// Info messages are basically information about commands processed on a
// room, or emote messages
var isInfoMessage = (msgtype === 'm.emote' || eventType !== 'm.room.message');
var isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
// This shouldn't happen: the caller should check we support this type
@ -375,25 +393,26 @@ module.exports = React.createClass({
throw new Error("Event type not supported");
}
var e2eEnabled = MatrixClientPeg.get().isRoomEncrypted(this.props.mxEvent.getRoomId());
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
var classes = classNames({
mx_EventTile: true,
mx_EventTile_info: isInfoMessage,
mx_EventTile_sending: ['sending', 'queued'].indexOf(
this.props.eventSendStatus
) !== -1,
mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting',
mx_EventTile_sending: isSending,
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_highlight: this.props.tileShape == 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.continuation,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_verified: this.state.verified == true || (e2eEnabled && isSending),
mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
});
var timestamp = <a href={ "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</a>
var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId();
var readAvatars = this.getReadAvatars();
@ -401,8 +420,11 @@ module.exports = React.createClass({
let avatarSize;
let needsSenderProfile;
if (isInfoMessage) {
// a small avatar, with no sender profile, for emotes and
if (this.props.tileShape === "notif") {
avatarSize = 24;
needsSenderProfile = true;
} else if (isInfoMessage) {
// a small avatar, with no sender profile, for
// joins/parts/etc
avatarSize = 14;
needsSenderProfile = false;
@ -428,35 +450,109 @@ module.exports = React.createClass({
if (needsSenderProfile) {
let aux = null;
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
if (!this.props.tileShape) {
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
else {
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
}
}
var editButton = (
<img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} />
);
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
var e2e;
if (e2eEnabled) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true) {
e2e = <img className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" alt="Encrypted by a verified device"/>;
}
else if (this.state.verified == false) {
e2e = <img className="mx_EventTile_e2eIcon" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt="Encrypted by an unverified device!"/>;
}
else {
e2e = <img className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
}
if (this.props.tileShape === "notif") {
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={ permalink }>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={ permalink }>
{ sender }
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</a>
</div>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
{ timestamp }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ editButton }
);
}
else if (this.props.tileShape === "file_grid") {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a className="mx_EventTile_senderDetailsLink" href={ permalink }>
<div className="mx_EventTile_senderDetails">
{ sender }
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</div>
</a>
</div>
</div>
);
);
}
else {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={ permalink }>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</a>
{ e2e }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ editButton }
</div>
</div>
);
}
},
});

View file

@ -54,33 +54,33 @@ export default class MemberDeviceInfo extends React.Component {
var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.isBlocked()) {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}>
Unblock
</div>
</button>
);
} else {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
onClick={this.onBlockClick}>
Block
</div>
</button>
);
}
if (this.props.device.isVerified()) {
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
Unverify
</div>
</button>
);
} else {
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
Verify
</div>
</button>
);
}
@ -101,16 +101,25 @@ export default class MemberDeviceInfo extends React.Component {
var deviceName = this.props.device.getDisplayName() || this.props.device.deviceId;
var info;
if (!this.props.hideInfo) {
info = (
<div>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator}
<div className="mx_MemberDeviceInfo_deviceKey">
{this.props.device.getFingerprint()}
</div>
</div>
);
}
// add the deviceId as a titletext to help with debugging
return (
<div className="mx_MemberDeviceInfo" title={this.props.device.deviceId}>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator}
<div className="mx_MemberDeviceInfo_deviceKey">
{this.props.device.getFingerprint()}
</div>
{verifyButton}
{blockButton}
{ info }
{ verifyButton }
{ blockButton }
</div>
);
}
@ -120,4 +129,5 @@ MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
hideInfo: React.PropTypes.bool,
};

View file

@ -421,11 +421,7 @@ module.exports = React.createClass({
onNewDMClick: function() {
this.setState({ updating: this.state.updating + 1 });
createRoom({
createOpts: {
invite: [this.props.member.userId],
},
}).finally(() => {
createRoom({dmUserId: this.props.member.userId}).finally(() => {
this.props.onFinished();
this.setState({ updating: this.state.updating - 1 });
}).done();

View file

@ -78,7 +78,7 @@ export default class MessageComposer extends React.Component {
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li>
fileList.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>);
}
@ -201,6 +201,13 @@ export default class MessageComposer extends React.Component {
</div>
);
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
// FIXME: show a /!\ if there are untrusted devices in the room...
controls.push(
<img key="e2eIcon" className="mx_MessageComposer_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" alt="Encrypted room"/>
);
}
var callButton, videoCallButton, hangupButton;
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =

View file

@ -147,8 +147,10 @@ module.exports = React.createClass({
this._updateStickyHeaders(true, scrollToPosition);
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
},

View file

@ -106,14 +106,16 @@ module.exports = React.createClass({
onMouseEnter: function() {
this.setState( { hover : true });
this.badgeOnMouseEnter();
},
onMouseLeave: function() {
this.setState( { hover : false });
this.badgeOnMouseLeave();
},
badgeOnMouseEnter: function() {
// Only allow none guests to access the context menu
// Only allow non-guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
@ -241,7 +243,7 @@ module.exports = React.createClass({
badgeContent = '\u200B';
}
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked}>{ badgeContent }</div>;
const EmojiText = sdk.getComponent('elements.EmojiText');
var label;