Merge remote-tracking branch 'origin/wmwragg/chat-multi-invite' into dbkr/wmwragg/chat-multi-invite

This commit is contained in:
David Baker 2016-09-13 14:51:46 +01:00
commit e95a6228e0
29 changed files with 861 additions and 204 deletions

View File

@ -57,4 +57,20 @@ export function inviteToRoom(roomId, addr) {
export function inviteMultipleToRoom(roomId, addrs) {
this.inviter = new MultiInviter(roomId);
return inviter.invite(addrs);
export function isValidAddress(addr) {
// Check if the addr is a valid type
var addrType = this.getAddressType(addr);
if (addrType === "mx") {
let user = MatrixClientPeg.get().getUser(addr);
if (user) {
return true;
} else {
return false;
}
} else if (addrType === "email") {
return true;
} else {
return false;
}
}

View File

@ -18,6 +18,8 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
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;
@ -104,6 +106,13 @@ class MatrixClientPeg {
this.matrixClient.setMaxListeners(500);
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) {
this.toolbarHidden = hidden;
// XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled
dis.dispatch({
@ -224,10 +224,12 @@ var Notifier = {
}
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
if (!room) return;
if (!this.isPrepared) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {

View File

@ -93,14 +93,12 @@ export function setDMRoom(roomId, userId) {
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)) {
const roomList = dmRoomMap[thisUserId];
if (thisUserId == userId) {
if (roomList.indexOf(roomId) == -1) {
roomList.push(roomId);
}
} else {
if (thisUserId != userId) {
const indexOfRoom = roomList.indexOf(roomId);
if (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);
}

View File

@ -37,7 +37,8 @@ class UserActivity {
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
// 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.lastDispatchAtTs = 0;
this.activityEndTimer = undefined;
@ -50,7 +51,8 @@ class UserActivity {
document.onmousedown = undefined;
document.onmousemove = 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['structures.ContextualMenu'] = require('./components/structures/ContextualMenu');
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.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.RoomView'] = require('./components/structures/RoomView');
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.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog');
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.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
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

@ -373,6 +373,9 @@ module.exports = React.createClass({
case 'view_create_chat':
this._createChat();
break;
case 'view_invite':
this._invite(payload.roomId);
break;
case 'notifier_enabled':
this.forceUpdate();
break;
@ -512,7 +515,17 @@ module.exports = React.createClass({
_createChat: function() {
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: "Start a one to one chat",
title: "Start a new chat",
});
},
_invite: function(roomId) {
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: "Invite new room members",
button: "Send Invites",
description: "Who would you like to add to this room?",
roomId: roomId,
});
},

View File

@ -60,6 +60,9 @@ module.exports = React.createClass({
// true to suppress the date at the start of the timeline
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
// scroll down when we are at the bottom of the window. See ScrollPanel
// for more details.
@ -73,6 +76,12 @@ module.exports = React.createClass({
// opacity for dynamic UI fading effects
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() {
@ -337,6 +346,7 @@ module.exports = React.createClass({
continuation = true;
}
/*
// 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
// 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') {
continuation = false;
}
*/
// local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators.
@ -370,7 +381,10 @@ module.exports = React.createClass({
// Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId;
var readReceipts = this._getReadReceiptsForEvent(mxEv);
var readReceipts;
if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
<li key={eventId}
@ -383,6 +397,7 @@ module.exports = React.createClass({
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
last={last} isSelectedEvent={highlight}/>
</li>
);
@ -503,7 +518,7 @@ module.exports = React.createClass({
style.opacity = this.props.opacity;
return (
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable"
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
onScroll={ this.props.onScroll }
onResize={ this.onResize }
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;
// ignore events for other rooms
if (!room) 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") {
this._updatePreviewUrlVisibility(room);
}
@ -1570,7 +1574,9 @@ module.exports = React.createClass({
var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
room={this.state.room}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={true}
manageReadMarkers={true}
hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId}
eventId={this.props.eventId}
@ -1579,6 +1585,7 @@ module.exports = React.createClass({
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview }
opacity={ this.props.opacity }
className="mx_RoomView_messagePanel"
/>);
var topUnreadMessagesBar = null;

View File

@ -50,9 +50,15 @@ var TimelinePanel = React.createClass({
displayName: 'TimelinePanel',
propTypes: {
// The js-sdk Room object for the room whose timeline we are
// representing.
room: React.PropTypes.object.isRequired,
// The js-sdk EventTimelineSet object for the timeline sequence we are
// representing. This may or may not have a room, depending on what it's
// 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.
hidden: React.PropTypes.bool,
@ -84,6 +90,12 @@ var TimelinePanel = React.createClass({
// maximum number of events to show in a timeline
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: {
@ -97,13 +109,18 @@ var TimelinePanel = React.createClass({
getDefaultProps: function() {
return {
timelineCap: 250,
className: 'mx_RoomView_messagePanel',
};
},
getInitialState: function() {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|| this._getCurrentReadReceipt();
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
if (this.props.manageReadMarkers) {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|| this._getCurrentReadReceipt();
}
return {
events: [],
@ -137,7 +154,7 @@ var TimelinePanel = React.createClass({
canForwardPaginate: false,
// 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,
readMarkerEventId: initialReadMarker,
@ -163,8 +180,8 @@ var TimelinePanel = React.createClass({
},
componentWillReceiveProps: function(newProps) {
if (newProps.room !== this.props.room) {
// throw new Error("changing room on a TimelinePanel is not supported");
if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
// regrettably, this does happen; in particular, when joining a
// 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
// 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) {
@ -280,11 +297,13 @@ var TimelinePanel = React.createClass({
this.props.onScroll();
}
// we hide the read marker when it first comes onto the screen, but if
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (this.getReadMarkerPosition() < 0) {
this.setState({readMarkerVisible: true});
if (this.props.manageReadMarkers) {
// we hide the read marker when it first comes onto the screen, but if
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (this.getReadMarkerPosition() < 0) {
this.setState({readMarkerVisible: true});
}
}
},
@ -304,8 +323,8 @@ var TimelinePanel = React.createClass({
},
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore events for other rooms
if (room !== this.props.room) return;
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
// ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
@ -337,40 +356,42 @@ var TimelinePanel = React.createClass({
var lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents());
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.room.getPendingEvents());
}
var updatedState = {events: events};
// when a new event arrives when the user is not watching the
// 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
// more than the timeout on userCurrentlyActive.
//
var myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null;
var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true;
} else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
if (this.props.manageReadMarkers) {
// when a new event arrives when the user is not watching the
// 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
// more than the timeout on userCurrentlyActive.
//
var myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null;
var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true;
} else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
}
}
this.setState(updatedState, callback);
});
},
onRoomTimelineReset: function(room) {
if (room !== this.props.room) return;
onRoomTimelineReset: function(room, timelineSet) {
if (timelineSet !== this.props.timelineSet) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
this._loadTimeline();
@ -381,7 +402,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// 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,
// but that's probably an early optimisation.
@ -392,7 +413,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.room) return;
if (room !== this.props.timelineSet.room) return;
this.forceUpdate();
},
@ -401,7 +422,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.room) return;
if (room !== this.props.timelineSet.room) return;
this._reloadEvents();
},
@ -409,12 +430,13 @@ var TimelinePanel = React.createClass({
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// 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.
if (this.isAtEndOfLiveTimeline()) {
this.props.room.setUnreadNotificationCount('total', 0);
this.props.room.setUnreadNotificationCount('highlight', 0);
this.props.timelineSet.room.setUnreadNotificationCount('total', 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
}
@ -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
// of the screen, so move the marker down to the bottom of the screen.
updateReadMarker: function() {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() !== 0) {
return;
}
@ -498,6 +521,8 @@ var TimelinePanel = React.createClass({
// advance the read marker past any events we sent ourselves.
_advanceReadMarkerPastMyEvents: function() {
if (!this.props.manageReadMarkers) return;
// we call _timelineWindow.getEvents() rather than using
// this.state.events, because react batches the update to the latter, so it
// may not have been updated yet.
@ -548,11 +573,9 @@ var TimelinePanel = React.createClass({
* the container.
*/
jumpToReadMarker: function() {
if (!this.refs.messagePanel)
return;
if (!this.state.readMarkerEventId)
return;
if (!this.props.manageReadMarkers) return;
if (!this.refs.messagePanel) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
// 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
*/
forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return;
var rmId = this._getCurrentReadReceipt();
// 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;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
@ -622,7 +647,9 @@ var TimelinePanel = React.createClass({
// 0: read marker is visible
// +1: read marker is below the window
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();
if (ret !== null) {
return ret;
@ -630,7 +657,7 @@ var TimelinePanel = React.createClass({
// 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.
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[0].getTs()) {
return -1;
@ -691,7 +718,7 @@ var TimelinePanel = React.createClass({
*/
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow(
MatrixClientPeg.get(), this.props.room,
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
var onLoaded = () => {
@ -745,7 +772,7 @@ var TimelinePanel = React.createClass({
// go via the dispatcher so that the URL is updated
dis.dispatch({
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 (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents());
events.push(... this.props.timelineSet.getPendingEvents());
}
return events;
@ -873,11 +900,13 @@ var TimelinePanel = React.createClass({
return null;
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) {
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
// no change to the RM.
return;
@ -885,11 +914,11 @@ var TimelinePanel = React.createClass({
// ideally we'd sync these via the server, but for now just stash them
// 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
// above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs;
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
if (inhibitSetState) {
return;
@ -919,7 +948,7 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper">
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<Loader />
</div>
);
@ -946,11 +975,14 @@ var TimelinePanel = React.createClass({
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
opacity={ this.props.opacity }
className={ this.props.className }
tileShape={ this.props.tileShape }
/>
);
},

View File

@ -37,6 +37,7 @@ module.exports = React.createClass({
]),
value: React.PropTypes.string,
placeholder: React.PropTypes.string,
roomId: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
@ -55,6 +56,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
error: false,
inviteList: [],
queryList: [],
};
@ -68,7 +70,7 @@ module.exports = React.createClass({
this._updateUserList();
},
onStartChat: function() {
onButtonClick: function() {
var addr;
// Either an address tile was created, or text input is being used
@ -76,8 +78,8 @@ module.exports = React.createClass({
addr = this.state.inviteList[0];
}
// Check if the addr is a valid type
if (Invite.getAddressType(addr) === "mx") {
if (this.state.inviteList.length === 1 && Invite.getAddressType(addr) === "mx" && !this.props.roomId) {
// Direct Message chat
var room = this._getDirectMessageRoom(addr);
if (room) {
// A Direct Message room already exists for this user and you
@ -90,11 +92,9 @@ module.exports = React.createClass({
} else {
this._startChat(addr);
}
} else if (Invite.getAddressType(addr) === "email") {
this._startChat(addr);
} else {
// Nothing to do, so focus back on the textinput
this.refs.textinput.focus();
// Multi invite chat
this._startChat(addr);
}
},
@ -122,12 +122,16 @@ module.exports = React.createClass({
} else if (e.keyCode === 32 || e.keyCode === 188) { // space or comma
e.stopPropagation();
e.preventDefault();
var inviteList = this.state.inviteList.slice();
inviteList.push(this.refs.textinput.value);
this.setState({
inviteList: inviteList,
queryList: [],
});
if (Invite.isValidAddress(this.refs.textinput.value)) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.refs.textinput.value);
this.setState({
inviteList: inviteList,
queryList: [],
});
} else {
this.setState({ error: true });
}
}
},
@ -142,7 +146,10 @@ module.exports = React.createClass({
});
}
this.setState({ queryList: queryList });
this.setState({
queryList: queryList,
error: false,
});
},
onDismissed: function(index) {
@ -181,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;
}
}
}
}
@ -189,19 +199,30 @@ module.exports = React.createClass({
},
_startChat: function(addr) {
// Start the chat
createRoom().then(function(roomId) {
return Invite.inviteToRoom(roomId, addr);
})
.catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failure to invite user",
description: err.toString()
});
return null;
})
.done();
if (this.props.roomId) {
Invite.inviteToRoom(this.props.roomId, addr)
.catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failure to invite user",
description: err.toString()
});
return null;
})
.done();
} else {
// 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);
@ -267,6 +288,7 @@ module.exports = React.createClass({
);
}
}
// Add the query at the end
query.push(
<textarea key={this.state.inviteList.length}
@ -281,6 +303,19 @@ module.exports = React.createClass({
</textarea>
);
var error;
var addressSelector;
if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>
} else {
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST } />
);
}
return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
@ -294,13 +329,11 @@ module.exports = React.createClass({
</div>
<div className="mx_Dialog_content">
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST } />
{ error }
{ addressSelector }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onStartChat}>
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
{this.props.button}
</button>
</div>

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;

View File

@ -18,6 +18,7 @@ var MatrixClientPeg = require('./MatrixClientPeg');
var Modal = require('./Modal');
var sdk = require('./index');
var dis = require("./dispatcher");
var Rooms = require("./Rooms");
var q = require('q');
@ -28,16 +29,17 @@ var q = require('q');
* action was aborted or failed.
*
* @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.
*/
function createRoom(opts) {
var opts = opts || {};
opts = opts || {};
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
var Loader = sdk.getComponent("elements.Spinner");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
const Loader = sdk.getComponent("elements.Spinner");
var client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
if (client.isGuest()) {
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
@ -46,10 +48,15 @@ function createRoom(opts) {
return q(null);
}
const defaultPreset = opts.dmUserId ? 'trusted_private_chat' : 'private_chat';
// set some defaults for the creation
var createOpts = opts.createOpts || {};
createOpts.preset = createOpts.preset || 'private_chat';
const createOpts = opts.createOpts || {};
createOpts.preset = createOpts.preset || defaultPreset;
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
// 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() {
modal.close();
}).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
// room has been created, so we race here with the client knowing that
// the room exists, causing things like
// https://github.com/vector-im/vector-web/issues/1813
dis.dispatch({
action: 'view_room',
room_id: res.room_id
room_id: roomId
});
return res.room_id;
return roomId;
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Failure to create room",

View File

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

View File

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