Merge up from develop
This commit is contained in:
commit
524eeaa315
27 changed files with 780 additions and 171 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
16
src/Rooms.js
16
src/Rooms.js
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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');
|
||||||
|
|
121
src/components/structures/FilePanel.js
Normal file
121
src/components/structures/FilePanel.js
Normal 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;
|
|
@ -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 }
|
||||||
|
|
65
src/components/structures/NotificationPanel.js
Normal file
65
src/components/structures/NotificationPanel.js
Normal 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;
|
|
@ -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;
|
||||||
|
|
|
@ -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 }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
123
src/components/views/dialogs/EncryptedEventDialog.js
Normal file
123
src/components/views/dialogs/EncryptedEventDialog.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
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>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue