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

@ -18,6 +18,8 @@ limitations under the License.
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils'; import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
const localStorage = window.localStorage; const localStorage = window.localStorage;
@ -104,6 +106,13 @@ class MatrixClientPeg {
this.matrixClient.setMaxListeners(500); this.matrixClient.setMaxListeners(500);
this.matrixClient.setGuest(Boolean(creds.guest)); this.matrixClient.setGuest(Boolean(creds.guest));
var notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true
});
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
this.matrixClient.setNotifTimelineSet(notifTimelineSet);
} }
} }

View file

@ -190,7 +190,7 @@ var Notifier = {
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden; this.toolbarHidden = hidden;
// XXX: why are we dispatching this here? // XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({
@ -224,10 +224,12 @@ var Notifier = {
} }
}, },
onRoomTimeline: function(ev, room, toStartOfTimeline) { onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
if (!room) return;
if (!this.isPrepared) return; // don't alert for any messages initially if (!this.isPrepared) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) { if (actions && actions.notify) {

View file

@ -93,14 +93,12 @@ export function setDMRoom(roomId, userId) {
if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent(); if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent();
// remove it from the lists of any others users
// (it can only be a DM room for one person)
for (const thisUserId of Object.keys(dmRoomMap)) { for (const thisUserId of Object.keys(dmRoomMap)) {
const roomList = dmRoomMap[thisUserId]; const roomList = dmRoomMap[thisUserId];
if (thisUserId == userId) { if (thisUserId != userId) {
if (roomList.indexOf(roomId) == -1) {
roomList.push(roomId);
}
} else {
const indexOfRoom = roomList.indexOf(roomId); const indexOfRoom = roomList.indexOf(roomId);
if (indexOfRoom > -1) { if (indexOfRoom > -1) {
roomList.splice(indexOfRoom, 1); roomList.splice(indexOfRoom, 1);
@ -108,6 +106,14 @@ export function setDMRoom(roomId, userId) {
} }
} }
// now add it, if it's not already there
const roomList = dmRoomMap[userId] || [];
if (roomList.indexOf(roomId) == -1) {
roomList.push(roomId);
}
dmRoomMap[userId] = roomList;
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
} }

View file

@ -37,7 +37,8 @@ class UserActivity {
// itself being scrolled. Need to use addEventListener's useCapture. // itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is // also this needs to be the wheel event, not scroll, as scroll is
// fired when the view scrolls down for a new message. // fired when the view scrolls down for a new message.
window.addEventListener('wheel', this._onUserActivity.bind(this), true); window.addEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true });
this.lastActivityAtTs = new Date().getTime(); this.lastActivityAtTs = new Date().getTime();
this.lastDispatchAtTs = 0; this.lastDispatchAtTs = 0;
this.activityEndTimer = undefined; this.activityEndTimer = undefined;
@ -50,7 +51,8 @@ class UserActivity {
document.onmousedown = undefined; document.onmousedown = undefined;
document.onmousemove = undefined; document.onmousemove = undefined;
document.onkeypress = undefined; document.onkeypress = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this), true); window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true });
} }
/** /**

View file

@ -27,8 +27,10 @@ limitations under the License.
module.exports.components = {}; module.exports.components = {};
module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu'); module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu');
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
module.exports.components['structures.FilePanel'] = require('./components/structures/FilePanel');
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');
module.exports.components['structures.NotificationPanel'] = require('./components/structures/NotificationPanel');
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar'); module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar');
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView'); module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
@ -47,6 +49,7 @@ module.exports.components['views.create_room.Presets'] = require('./components/v
module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias'); module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias');
module.exports.components['views.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog'); module.exports.components['views.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog');
module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog'); module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog');
module.exports.components['views.dialogs.EncryptedEventDialog'] = require('./components/views/dialogs/EncryptedEventDialog');
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
module.exports.components['views.dialogs.MultiInviteDialog'] = require('./components/views/dialogs/MultiInviteDialog'); module.exports.components['views.dialogs.MultiInviteDialog'] = require('./components/views/dialogs/MultiInviteDialog');

View file

@ -0,0 +1,121 @@
/*
Copyright 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 ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
/*
* Component which shows the filtered file using a TimelinePanel
*/
var FilePanel = React.createClass({
displayName: 'FilePanel',
propTypes: {
roomId: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
timelineSet: null,
}
},
componentWillMount: function() {
this.updateTimelineSet(this.props.roomId);
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.roomId !== this.props.roomId) {
// otherwise we race between re-rendering the TimelinePanel and setting the new timelineSet.
//
// FIXME: this race only happens because of the promise returned by getOrCreateFilter().
// We should only need to create the containsUrl filter once per login session, so in practice
// it shouldn't be being done here at all. Then we could just update the timelineSet directly
// without resetting it first, and speed up room-change.
this.setState({ timelineSet: null });
this.updateTimelineSet(nextProps.roomId);
}
},
updateTimelineSet: function(roomId) {
var client = MatrixClientPeg.get();
var room = client.getRoom(roomId);
if (room) {
var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true
},
}
}
);
// FIXME: we shouldn't be doing this every time we change room - see comment above.
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{
filter.filterId = filterId;
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
this.setState({ timelineSet: timelineSet });
},
(error)=>{
console.error("Failed to get or create file panel filter", error);
}
);
}
else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
},
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
return (
<TimelinePanel key={"filepanel_" + this.props.roomId}
className="mx_FilePanel"
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = { false }
tileShape="file_grid"
opacity={ this.props.opacity }
/>
);
}
else {
return (
<div className="mx_FilePanel">
<Loader/>
</div>
);
}
},
});
module.exports = FilePanel;

View file

@ -60,6 +60,9 @@ module.exports = React.createClass({
// true to suppress the date at the start of the timeline // true to suppress the date at the start of the timeline
suppressFirstDateSeparator: React.PropTypes.bool, suppressFirstDateSeparator: React.PropTypes.bool,
// whether to show read receipts
manageReadReceipts: React.PropTypes.bool,
// true if updates to the event list should cause the scroll panel to // true if updates to the event list should cause the scroll panel to
// scroll down when we are at the bottom of the window. See ScrollPanel // scroll down when we are at the bottom of the window. See ScrollPanel
// for more details. // for more details.
@ -73,6 +76,12 @@ module.exports = React.createClass({
// opacity for dynamic UI fading effects // opacity for dynamic UI fading effects
opacity: React.PropTypes.number, opacity: React.PropTypes.number,
// className for the panel
className: React.PropTypes.string.isRequired,
// shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -337,6 +346,7 @@ module.exports = React.createClass({
continuation = true; continuation = true;
} }
/*
// Work out if this is still a continuation, as we are now showing commands // Work out if this is still a continuation, as we are now showing commands
// and /me messages with their own little avatar. The case of a change of // and /me messages with their own little avatar. The case of a change of
// event type (commands) is handled above, but we need to handle the /me // event type (commands) is handled above, but we need to handle the /me
@ -348,6 +358,7 @@ module.exports = React.createClass({
&& prevEvent.getContent().msgtype === 'm.emote') { && prevEvent.getContent().msgtype === 'm.emote') {
continuation = false; continuation = false;
} }
*/
// local echoes have a fake date, which could even be yesterday. Treat them // local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators. // as 'today' for the date separators.
@ -370,7 +381,10 @@ module.exports = React.createClass({
// Local echos have a send "status". // Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId; var scrollToken = mxEv.status ? undefined : eventId;
var readReceipts = this._getReadReceiptsForEvent(mxEv); var readReceipts;
if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push( ret.push(
<li key={eventId} <li key={eventId}
@ -383,6 +397,7 @@ module.exports = React.createClass({
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting} checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status} eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
last={last} isSelectedEvent={highlight}/> last={last} isSelectedEvent={highlight}/>
</li> </li>
); );
@ -503,7 +518,7 @@ module.exports = React.createClass({
style.opacity = this.props.opacity; style.opacity = this.props.opacity;
return ( return (
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable" <ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
onScroll={ this.props.onScroll } onScroll={ this.props.onScroll }
onResize={ this.onResize } onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest } onFillRequest={ this.props.onFillRequest }

View file

@ -0,0 +1,65 @@
/*
Copyright 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 ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel
*/
var NotificationPanel = React.createClass({
displayName: 'NotificationPanel',
propTypes: {
},
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
className="mx_NotificationPanel"
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview = { false }
opacity={ this.props.opacity }
tileShape="notif"
/>
);
}
else {
console.error("No notifTimelineSet available!");
return (
<div className="mx_NotificationPanel">
<Loader/>
</div>
);
}
},
});
module.exports = NotificationPanel;

View file

@ -340,8 +340,12 @@ module.exports = React.createClass({
if (this.unmounted) return; if (this.unmounted) return;
// ignore events for other rooms // ignore events for other rooms
if (!room) return;
if (!this.state.room || room.roomId != this.state.room.roomId) return; if (!this.state.room || room.roomId != this.state.room.roomId) return;
// ignore events from filtered timelines
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
if (ev.getType() === "org.matrix.room.preview_urls") { if (ev.getType() === "org.matrix.room.preview_urls") {
this._updatePreviewUrlVisibility(room); this._updatePreviewUrlVisibility(room);
} }
@ -1570,7 +1574,9 @@ module.exports = React.createClass({
var messagePanel = ( var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
room={this.state.room} timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={true}
manageReadMarkers={true}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId} highlightedEventId={this.props.highlightedEventId}
eventId={this.props.eventId} eventId={this.props.eventId}
@ -1579,6 +1585,7 @@ module.exports = React.createClass({
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview } showUrlPreview = { this.state.showUrlPreview }
opacity={ this.props.opacity } opacity={ this.props.opacity }
className="mx_RoomView_messagePanel"
/>); />);
var topUnreadMessagesBar = null; var topUnreadMessagesBar = null;

View file

@ -50,9 +50,15 @@ var TimelinePanel = React.createClass({
displayName: 'TimelinePanel', displayName: 'TimelinePanel',
propTypes: { propTypes: {
// The js-sdk Room object for the room whose timeline we are // The js-sdk EventTimelineSet object for the timeline sequence we are
// representing. // representing. This may or may not have a room, depending on what it's
room: React.PropTypes.object.isRequired, // a timeline representing. If it has a room, we maintain RRs etc for
// that room.
timelineSet: React.PropTypes.object.isRequired,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool,
manageReadMarkers: React.PropTypes.bool,
// true to give the component a 'display: none' style. // true to give the component a 'display: none' style.
hidden: React.PropTypes.bool, hidden: React.PropTypes.bool,
@ -84,6 +90,12 @@ var TimelinePanel = React.createClass({
// maximum number of events to show in a timeline // maximum number of events to show in a timeline
timelineCap: React.PropTypes.number, timelineCap: React.PropTypes.number,
// classname to use for the messagepanel
className: React.PropTypes.string,
// shape property to be passed to EventTiles
tileShape: React.PropTypes.string,
}, },
statics: { statics: {
@ -97,13 +109,18 @@ var TimelinePanel = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
timelineCap: 250, timelineCap: 250,
className: 'mx_RoomView_messagePanel',
}; };
}, },
getInitialState: function() { getInitialState: function() {
var initialReadMarker = // XXX: we could track RM per TimelineSet rather than per Room.
TimelinePanel.roomReadMarkerMap[this.props.room.roomId] // but for now we just do it per room for simplicity.
|| this._getCurrentReadReceipt(); if (this.props.manageReadMarkers) {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|| this._getCurrentReadReceipt();
}
return { return {
events: [], events: [],
@ -137,7 +154,7 @@ var TimelinePanel = React.createClass({
canForwardPaginate: false, canForwardPaginate: false,
// start with the read-marker visible, so that we see its animated // start with the read-marker visible, so that we see its animated
// disappearance when swtitching into the room. // disappearance when switching into the room.
readMarkerVisible: true, readMarkerVisible: true,
readMarkerEventId: initialReadMarker, readMarkerEventId: initialReadMarker,
@ -163,8 +180,8 @@ var TimelinePanel = React.createClass({
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
if (newProps.room !== this.props.room) { if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing room on a TimelinePanel is not supported"); // throw new Error("changing timelineSet on a TimelinePanel is not supported");
// regrettably, this does happen; in particular, when joining a // regrettably, this does happen; in particular, when joining a
// room with /join. In that case, there are two Rooms in // room with /join. In that case, there are two Rooms in
@ -175,7 +192,7 @@ var TimelinePanel = React.createClass({
// //
// for now, just warn about this. But we're going to end up paginating // for now, just warn about this. But we're going to end up paginating
// both rooms separately, and it's all bad. // both rooms separately, and it's all bad.
console.warn("Replacing room on a TimelinePanel - confusion may ensue"); console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
} }
if (newProps.eventId != this.props.eventId) { if (newProps.eventId != this.props.eventId) {
@ -280,11 +297,13 @@ var TimelinePanel = React.createClass({
this.props.onScroll(); this.props.onScroll();
} }
// we hide the read marker when it first comes onto the screen, but if if (this.props.manageReadMarkers) {
// it goes back off the top of the screen (presumably because the user // we hide the read marker when it first comes onto the screen, but if
// clicks on the 'jump to bottom' button), we need to re-enable it. // it goes back off the top of the screen (presumably because the user
if (this.getReadMarkerPosition() < 0) { // clicks on the 'jump to bottom' button), we need to re-enable it.
this.setState({readMarkerVisible: true}); if (this.getReadMarkerPosition() < 0) {
this.setState({readMarkerVisible: true});
}
} }
}, },
@ -304,8 +323,8 @@ var TimelinePanel = React.createClass({
}, },
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore events for other rooms // ignore events for other timeline sets
if (room !== this.props.room) return; if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
// ignore anything but real-time updates at the end of the room: // ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes. // updates from pagination will happen when the paginate completes.
@ -337,40 +356,42 @@ var TimelinePanel = React.createClass({
var lastEv = events[events.length-1]; var lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents()); events.push(... this.props.timelineSet.room.getPendingEvents());
} }
var updatedState = {events: events}; var updatedState = {events: events};
// when a new event arrives when the user is not watching the if (this.props.manageReadMarkers) {
// window, but the window is in its auto-scroll mode, make sure the // when a new event arrives when the user is not watching the
// read marker is visible. // window, but the window is in its auto-scroll mode, make sure the
// // read marker is visible.
// We ignore events we have sent ourselves; we don't want to see the //
// read-marker when a remote echo of an event we have just sent takes // We ignore events we have sent ourselves; we don't want to see the
// more than the timeout on userCurrentlyActive. // read-marker when a remote echo of an event we have just sent takes
// // more than the timeout on userCurrentlyActive.
var myUserId = MatrixClientPeg.get().credentials.userId; //
var sender = ev.sender ? ev.sender.userId : null; var myUserId = MatrixClientPeg.get().credentials.userId;
var callback = null; var sender = ev.sender ? ev.sender.userId : null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) { var callback = null;
updatedState.readMarkerVisible = true; if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
} else if(lastEv && this.getReadMarkerPosition() === 0) { updatedState.readMarkerVisible = true;
// we know we're stuckAtBottom, so we can advance the RM } else if(lastEv && this.getReadMarkerPosition() === 0) {
// immediately, to save a later render cycle // we know we're stuckAtBottom, so we can advance the RM
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); // immediately, to save a later render cycle
updatedState.readMarkerVisible = false; this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerEventId = lastEv.getId(); updatedState.readMarkerVisible = false;
callback = this.props.onReadMarkerUpdated; updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
}
} }
this.setState(updatedState, callback); this.setState(updatedState, callback);
}); });
}, },
onRoomTimelineReset: function(room) { onRoomTimelineReset: function(room, timelineSet) {
if (room !== this.props.room) return; if (timelineSet !== this.props.timelineSet) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) { if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
this._loadTimeline(); this._loadTimeline();
@ -381,7 +402,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return; if (this.unmounted) return;
// ignore events for other rooms // ignore events for other rooms
if (room !== this.props.room) return; if (room !== this.props.timelineSet.room) return;
// we could skip an update if the event isn't in our timeline, // we could skip an update if the event isn't in our timeline,
// but that's probably an early optimisation. // but that's probably an early optimisation.
@ -392,7 +413,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return; if (this.unmounted) return;
// ignore events for other rooms // ignore events for other rooms
if (room !== this.props.room) return; if (room !== this.props.timelineSet.room) return;
this.forceUpdate(); this.forceUpdate();
}, },
@ -401,7 +422,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return; if (this.unmounted) return;
// ignore events for other rooms // ignore events for other rooms
if (room !== this.props.room) return; if (room !== this.props.timelineSet.room) return;
this._reloadEvents(); this._reloadEvents();
}, },
@ -409,12 +430,13 @@ var TimelinePanel = React.createClass({
sendReadReceipt: function() { sendReadReceipt: function() {
if (!this.refs.messagePanel) return; if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
// to avoid having to wait from the remote echo from the homeserver. // to avoid having to wait from the remote echo from the homeserver.
if (this.isAtEndOfLiveTimeline()) { if (this.isAtEndOfLiveTimeline()) {
this.props.room.setUnreadNotificationCount('total', 0); this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
this.props.room.setUnreadNotificationCount('highlight', 0); this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
// XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up // XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up
} }
@ -461,6 +483,7 @@ var TimelinePanel = React.createClass({
// if the read marker is on the screen, we can now assume we've caught up to the end // if the read marker is on the screen, we can now assume we've caught up to the end
// of the screen, so move the marker down to the bottom of the screen. // of the screen, so move the marker down to the bottom of the screen.
updateReadMarker: function() { updateReadMarker: function() {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() !== 0) { if (this.getReadMarkerPosition() !== 0) {
return; return;
} }
@ -498,6 +521,8 @@ var TimelinePanel = React.createClass({
// advance the read marker past any events we sent ourselves. // advance the read marker past any events we sent ourselves.
_advanceReadMarkerPastMyEvents: function() { _advanceReadMarkerPastMyEvents: function() {
if (!this.props.manageReadMarkers) return;
// we call _timelineWindow.getEvents() rather than using // we call _timelineWindow.getEvents() rather than using
// this.state.events, because react batches the update to the latter, so it // this.state.events, because react batches the update to the latter, so it
// may not have been updated yet. // may not have been updated yet.
@ -548,11 +573,9 @@ var TimelinePanel = React.createClass({
* the container. * the container.
*/ */
jumpToReadMarker: function() { jumpToReadMarker: function() {
if (!this.refs.messagePanel) if (!this.props.manageReadMarkers) return;
return; if (!this.refs.messagePanel) return;
if (!this.state.readMarkerEventId) return;
if (!this.state.readMarkerEventId)
return;
// we may not have loaded the event corresponding to the read-marker // we may not have loaded the event corresponding to the read-marker
// into the _timelineWindow. In that case, attempts to scroll to it // into the _timelineWindow. In that case, attempts to scroll to it
@ -579,10 +602,12 @@ var TimelinePanel = React.createClass({
/* update the read-up-to marker to match the read receipt /* update the read-up-to marker to match the read receipt
*/ */
forgetReadMarker: function() { forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return;
var rmId = this._getCurrentReadReceipt(); var rmId = this._getCurrentReadReceipt();
// see if we know the timestamp for the rr event // see if we know the timestamp for the rr event
var tl = this.props.room.getTimelineForEvent(rmId); var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs; var rmTs;
if (tl) { if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId }); var event = tl.getEvents().find((e) => { return e.getId() == rmId });
@ -622,7 +647,9 @@ var TimelinePanel = React.createClass({
// 0: read marker is visible // 0: read marker is visible
// +1: read marker is below the window // +1: read marker is below the window
getReadMarkerPosition: function() { getReadMarkerPosition: function() {
if (!this.refs.messagePanel) { return null; } if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
var ret = this.refs.messagePanel.getReadMarkerPosition(); var ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) { if (ret !== null) {
return ret; return ret;
@ -630,7 +657,7 @@ var TimelinePanel = React.createClass({
// the messagePanel doesn't know where the read marker is. // the messagePanel doesn't know where the read marker is.
// if we know the timestamp of the read marker, make a guess based on that. // if we know the timestamp of the read marker, make a guess based on that.
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId]; var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
if (rmTs && this.state.events.length > 0) { if (rmTs && this.state.events.length > 0) {
if (rmTs < this.state.events[0].getTs()) { if (rmTs < this.state.events[0].getTs()) {
return -1; return -1;
@ -691,7 +718,7 @@ var TimelinePanel = React.createClass({
*/ */
_loadTimeline: function(eventId, pixelOffset, offsetBase) { _loadTimeline: function(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow( this._timelineWindow = new Matrix.TimelineWindow(
MatrixClientPeg.get(), this.props.room, MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap}); {windowLimit: this.props.timelineCap});
var onLoaded = () => { var onLoaded = () => {
@ -745,7 +772,7 @@ var TimelinePanel = React.createClass({
// go via the dispatcher so that the URL is updated // go via the dispatcher so that the URL is updated
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: this.props.room.roomId, room_id: this.props.timelineSet.roomId,
}); });
}; };
} }
@ -807,7 +834,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents()); events.push(... this.props.timelineSet.getPendingEvents());
} }
return events; return events;
@ -873,11 +900,13 @@ var TimelinePanel = React.createClass({
return null; return null;
var myUserId = client.credentials.userId; var myUserId = client.credentials.userId;
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized); return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
}, },
_setReadMarker: function(eventId, eventTs, inhibitSetState) { _setReadMarker: function(eventId, eventTs, inhibitSetState) {
if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) { var roomId = this.props.timelineSet.room.roomId;
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
// don't update the state (and cause a re-render) if there is // don't update the state (and cause a re-render) if there is
// no change to the RM. // no change to the RM.
return; return;
@ -885,11 +914,11 @@ var TimelinePanel = React.createClass({
// ideally we'd sync these via the server, but for now just stash them // ideally we'd sync these via the server, but for now just stash them
// in a map. // in a map.
TimelinePanel.roomReadMarkerMap[this.props.room.roomId] = eventId; TimelinePanel.roomReadMarkerMap[roomId] = eventId;
// in order to later figure out if the read marker is // in order to later figure out if the read marker is
// above or below the visible timeline, we stash the timestamp. // above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs; TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
if (inhibitSetState) { if (inhibitSetState) {
return; return;
@ -919,7 +948,7 @@ var TimelinePanel = React.createClass({
// exist. // exist.
if (this.state.timelineLoading) { if (this.state.timelineLoading) {
return ( return (
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper"> <div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<Loader /> <Loader />
</div> </div>
); );
@ -946,11 +975,14 @@ var TimelinePanel = React.createClass({
readMarkerVisible={ this.state.readMarkerVisible } readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate } suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview } showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId } ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom } stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest } onFillRequest={ this.onMessageListFillRequest }
opacity={ this.props.opacity } opacity={ this.props.opacity }
className={ this.props.className }
tileShape={ this.props.tileShape }
/> />
); );
}, },

View file

@ -188,7 +188,10 @@ module.exports = React.createClass({
for (let i = 0; i < dmRooms.length; i++) { for (let i = 0; i < dmRooms.length; i++) {
let room = MatrixClientPeg.get().getRoom(dmRooms[i]); let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
if (room) { 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(); .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 // Close - this will happen before the above, as that is async
this.props.onFinished(true, addr); 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({ module.exports = React.createClass({
displayName: 'LogoutPrompt', displayName: 'LogoutPrompt',
propTypes: {
onFinished: React.PropTypes.func,
},
logOut: function() { logOut: function() {
dis.dispatch({action: 'logout'}); dis.dispatch({action: 'logout'});
if (this.props.onFinished) { if (this.props.onFinished) {

View file

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

View file

@ -123,6 +123,30 @@ module.exports = React.createClass({
var content = this.props.mxEvent.getContent(); var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get(); 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(); var thumbUrl = this._getThumbUrl();
if (thumbUrl) { if (thumbUrl) {
return ( return (
@ -133,12 +157,7 @@ module.exports = React.createClass({
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} /> onMouseLeave={this.onImageLeave} />
</a> </a>
<div className="mx_MImageBody_download"> { 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>
</span> </span>
); );
} else if (content.body) { } 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 ( return (
<span className="mx_MVideoBody"> <span className="mx_MVideoBody">
<video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body} <video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
controls preload={preload} autoPlay={false} controls preload={preload} autoPlay={false}
height={height} width={width} poster={poster}> height={height} width={width} poster={poster}>
</video> </video>
{ download }
</span> </span>
); );
}, },

View file

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

View file

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

View file

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var classNames = require("classnames"); var classNames = require("classnames");
var Modal = require('../../../Modal');
var sdk = require('../../../index'); var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg') var MatrixClientPeg = require('../../../MatrixClientPeg')
@ -128,6 +129,15 @@ module.exports = React.createClass({
/* the status of this event - ie, mxEvent.status. Denormalised to here so /* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */ * that we can tell when it changes. */
eventSendStatus: React.PropTypes.string, 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() { getInitialState: function() {
@ -239,8 +249,7 @@ module.exports = React.createClass({
if (!actions || !actions.tweaks) { return false; } if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients // don't show self-highlights from another of our clients
if (this.props.mxEvent.sender && if (this.props.mxEvent.getSender() === MatrixClientPeg.get().credentials.userId)
this.props.mxEvent.sender.userId === MatrixClientPeg.get().credentials.userId)
{ {
return false; 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() { render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile'); var SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -366,7 +384,7 @@ module.exports = React.createClass({
// Info messages are basically information about commands processed on a // Info messages are basically information about commands processed on a
// room, or emote messages // 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]); var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
// This shouldn't happen: the caller should check we support this type // 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"); 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({ var classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_info: isInfoMessage, mx_EventTile_info: isInfoMessage,
mx_EventTile_sending: ['sending', 'queued'].indexOf( mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting',
this.props.eventSendStatus mx_EventTile_sending: isSending,
) !== -1,
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent', 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_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_last: this.props.last,
mx_EventTile_contextual: this.props.contextual, mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu, 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_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() }> var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId();
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</a>
var readAvatars = this.getReadAvatars(); var readAvatars = this.getReadAvatars();
@ -401,8 +420,11 @@ module.exports = React.createClass({
let avatarSize; let avatarSize;
let needsSenderProfile; let needsSenderProfile;
if (isInfoMessage) { if (this.props.tileShape === "notif") {
// a small avatar, with no sender profile, for emotes and avatarSize = 24;
needsSenderProfile = true;
} else if (isInfoMessage) {
// a small avatar, with no sender profile, for
// joins/parts/etc // joins/parts/etc
avatarSize = 14; avatarSize = 14;
needsSenderProfile = false; needsSenderProfile = false;
@ -428,35 +450,109 @@ module.exports = React.createClass({
if (needsSenderProfile) { if (needsSenderProfile) {
let aux = null; let aux = null;
if (msgtype === 'm.image') aux = "sent an image"; if (!this.props.tileShape) {
else if (msgtype === 'm.video') aux = "sent a video"; if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.file') aux = "uploaded a file"; 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} />; sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
else {
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
}
} }
var editButton = ( var editButton = (
<img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} /> <img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} />
); );
return ( var e2e;
<div className={classes}> if (e2eEnabled) {
<div className="mx_EventTile_msgOption"> if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
{ readAvatars } 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> </div>
{ avatar } );
{ sender } }
<div className="mx_EventTile_line"> else if (this.props.tileShape === "file_grid") {
{ timestamp } return (
<EventTileType ref="tile" <div className={classes}>
mxEvent={this.props.mxEvent} <div className="mx_EventTile_line" >
highlights={this.props.highlights} <EventTileType ref="tile"
highlightLink={this.props.highlightLink} mxEvent={this.props.mxEvent}
showUrlPreview={this.props.showUrlPreview} highlights={this.props.highlights}
onWidgetLoad={this.props.onWidgetLoad} /> highlightLink={this.props.highlightLink}
{ editButton } 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>
</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; var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.isBlocked()) { if (this.props.device.isBlocked()) {
blockButton = ( blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}> onClick={this.onUnblockClick}>
Unblock Unblock
</div> </button>
); );
} else { } else {
blockButton = ( blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
onClick={this.onBlockClick}> onClick={this.onBlockClick}>
Block Block
</div> </button>
); );
} }
if (this.props.device.isVerified()) { if (this.props.device.isVerified()) {
verifyButton = ( verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
Unverify Unverify
</div> </button>
); );
} else { } else {
verifyButton = ( verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}> onClick={this.onVerifyClick}>
Verify 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 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 // add the deviceId as a titletext to help with debugging
return ( return (
<div className="mx_MemberDeviceInfo" title={this.props.device.deviceId}> <div className="mx_MemberDeviceInfo" title={this.props.device.deviceId}>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div> { info }
{indicator} { verifyButton }
<div className="mx_MemberDeviceInfo_deviceKey"> { blockButton }
{this.props.device.getFingerprint()}
</div>
{verifyButton}
{blockButton}
</div> </div>
); );
} }
@ -120,4 +129,5 @@ MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = { MemberDeviceInfo.propTypes = {
userId: React.PropTypes.string.isRequired, userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired, device: React.PropTypes.object.isRequired,
hideInfo: React.PropTypes.bool,
}; };

View file

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

View file

@ -78,7 +78,7 @@ export default class MessageComposer extends React.Component {
let fileList = []; let fileList = [];
for (let i=0; i<files.length; i++) { 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} <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>); </li>);
} }
@ -201,6 +201,13 @@ export default class MessageComposer extends React.Component {
</div> </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; var callButton, videoCallButton, hangupButton;
if (this.props.callState && this.props.callState !== 'ended') { if (this.props.callState && this.props.callState !== 'ended') {
hangupButton = hangupButton =

View file

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

View file

@ -106,14 +106,16 @@ module.exports = React.createClass({
onMouseEnter: function() { onMouseEnter: function() {
this.setState( { hover : true }); this.setState( { hover : true });
this.badgeOnMouseEnter();
}, },
onMouseLeave: function() { onMouseLeave: function() {
this.setState( { hover : false }); this.setState( { hover : false });
this.badgeOnMouseLeave();
}, },
badgeOnMouseEnter: function() { 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 // and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) { if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } ); this.setState( { badgeHover : true } );
@ -241,7 +243,7 @@ module.exports = React.createClass({
badgeContent = '\u200B'; 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'); const EmojiText = sdk.getComponent('elements.EmojiText');
var label; var label;

View file

@ -18,6 +18,7 @@ var MatrixClientPeg = require('./MatrixClientPeg');
var Modal = require('./Modal'); var Modal = require('./Modal');
var sdk = require('./index'); var sdk = require('./index');
var dis = require("./dispatcher"); var dis = require("./dispatcher");
var Rooms = require("./Rooms");
var q = require('q'); var q = require('q');
@ -28,16 +29,17 @@ var q = require('q');
* action was aborted or failed. * action was aborted or failed.
* *
* @param {object=} opts parameters for creating the room * @param {object=} opts parameters for creating the room
* @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them
* @param {object=} opts.createOpts set of options to pass to createRoom call. * @param {object=} opts.createOpts set of options to pass to createRoom call.
*/ */
function createRoom(opts) { function createRoom(opts) {
var opts = opts || {}; opts = opts || {};
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client.isGuest()) { if (client.isGuest()) {
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: "Please Register",
@ -46,10 +48,15 @@ function createRoom(opts) {
return q(null); return q(null);
} }
const defaultPreset = opts.dmUserId ? 'trusted_private_chat' : 'private_chat';
// set some defaults for the creation // set some defaults for the creation
var createOpts = opts.createOpts || {}; const createOpts = opts.createOpts || {};
createOpts.preset = createOpts.preset || 'private_chat'; createOpts.preset = createOpts.preset || defaultPreset;
createOpts.visibility = createOpts.visibility || 'private'; createOpts.visibility = createOpts.visibility || 'private';
if (opts.dmUserId && createOpts.invite === undefined) {
createOpts.invite = [opts.dmUserId];
}
// Allow guests by default since the room is private and they'd // Allow guests by default since the room is private and they'd
// need an invite. This means clicking on a 3pid invite email can // need an invite. This means clicking on a 3pid invite email can
@ -64,20 +71,28 @@ function createRoom(opts) {
} }
]; ];
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
let roomId;
return client.createRoom(createOpts).finally(function() { return client.createRoom(createOpts).finally(function() {
modal.close(); modal.close();
}).then(function(res) { }).then(function(res) {
roomId = res.room_id;
if (opts.dmUserId) {
return Rooms.setDMRoom(roomId, opts.dmUserId);
} else {
return q();
}
}).then(function() {
// NB createRoom doesn't block on the client seeing the echo that the // NB createRoom doesn't block on the client seeing the echo that the
// room has been created, so we race here with the client knowing that // room has been created, so we race here with the client knowing that
// the room exists, causing things like // the room exists, causing things like
// https://github.com/vector-im/vector-web/issues/1813 // https://github.com/vector-im/vector-web/issues/1813
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: res.room_id room_id: roomId
}); });
return res.room_id; return roomId;
}, function(err) { }, function(err) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failure to create room", title: "Failure to create room",

View file

@ -35,6 +35,7 @@ var USER_ID = '@me:localhost';
describe('TimelinePanel', function() { describe('TimelinePanel', function() {
var sandbox; var sandbox;
var timelineSet;
var room; var room;
var client; var client;
var timeline; var timeline;
@ -58,10 +59,16 @@ describe('TimelinePanel', function() {
test_utils.beforeEach(this); test_utils.beforeEach(this);
sandbox = test_utils.stubClient(sandbox); sandbox = test_utils.stubClient(sandbox);
timeline = new jssdk.EventTimeline(ROOM_ID);
room = sinon.createStubInstance(jssdk.Room); room = sinon.createStubInstance(jssdk.Room);
room.getLiveTimeline.returns(timeline); room.roomId = ROOM_ID;
room.getPendingEvents.returns([]);
timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);
timelineSet.getPendingEvents.returns([]);
timelineSet.room = room;
timeline = new jssdk.EventTimeline(timelineSet);
timelineSet.getLiveTimeline.returns(timeline);
client = peg.get(); client = peg.get();
client.credentials = {userId: USER_ID}; client.credentials = {userId: USER_ID};
@ -95,7 +102,7 @@ describe('TimelinePanel', function() {
var scrollDefer; var scrollDefer;
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} <TimelinePanel timelineSet={timelineSet} onScroll={() => {scrollDefer.resolve()}}
/>, />,
parentDiv, parentDiv,
); );
@ -143,7 +150,10 @@ describe('TimelinePanel', function() {
// a new event! // a new event!
var ev = mkMessage(); var ev = mkMessage();
timeline.addEvent(ev); timeline.addEvent(ev);
panel.onRoomTimeline(ev, room, false, false, {liveEvent: true}); panel.onRoomTimeline(ev, room, false, false, {
liveEvent: true,
timeline: timeline,
});
// that won't make much difference, because we don't paginate // that won't make much difference, because we don't paginate
// unless we're at the bottom of the timeline, but a scroll event // unless we're at the bottom of the timeline, but a scroll event
@ -178,7 +188,7 @@ describe('TimelinePanel', function() {
}); });
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room}/>, <TimelinePanel timelineSet={timelineSet}/>,
parentDiv parentDiv
); );
@ -226,7 +236,7 @@ describe('TimelinePanel', function() {
var scrollDefer; var scrollDefer;
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} <TimelinePanel timelineSet={timelineSet} onScroll={() => {scrollDefer.resolve()}}
timelineCap={TIMELINE_CAP} timelineCap={TIMELINE_CAP}
/>, />,
parentDiv parentDiv

View file

@ -39,6 +39,7 @@ export function stubClient() {
loginFlows: sinon.stub(), loginFlows: sinon.stub(),
on: sinon.stub(), on: sinon.stub(),
removeListener: sinon.stub(), removeListener: sinon.stub(),
isRoomEncrypted: sinon.stub().returns(false),
paginateEventTimeline: sinon.stub().returns(q()), paginateEventTimeline: sinon.stub().returns(q()),
sendReadReceipt: sinon.stub().returns(q()), sendReadReceipt: sinon.stub().returns(q()),