WIP switch-over of TimePanel from taking Rooms to taking EventTimelineSets

This commit is contained in:
Matthew Hodgson 2016-09-06 00:59:17 +01:00
parent 820cd579d8
commit e22d0a53b6
4 changed files with 139 additions and 68 deletions

View file

@ -27,6 +27,46 @@ var dis = require("../../dispatcher");
var FilePanel = React.createClass({ var FilePanel = React.createClass({
displayName: 'FilePanel', displayName: 'FilePanel',
propTypes: {
roomId: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
room: MatrixClientPeg.get().getRoom(this.props.roomId),
timelineSet: null,
}
},
componentWillMount: function() {
if (this.state.room) {
var client = MatrixClientPeg.get();
var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true
},
}
}
);
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{
var timelineSet = this.state.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!");
}
},
// this has to be a proper method rather than an unnamed function, // this has to be a proper method rather than an unnamed function,
// otherwise react calls it with null on each update. // otherwise react calls it with null on each update.
_gatherTimelinePanelRef: function(r) { _gatherTimelinePanelRef: function(r) {
@ -36,8 +76,6 @@ var FilePanel = React.createClass({
render: function() { render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
// <TimelinePanel ref={this._gatherTimelinePanelRef} // <TimelinePanel ref={this._gatherTimelinePanelRef}
// room={this.state.room} // room={this.state.room}
// hidden={hideMessagePanel} // hidden={hideMessagePanel}
@ -51,7 +89,9 @@ var FilePanel = React.createClass({
return ( return (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
room={this.state.room} manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = { false } showUrlPreview = { false }
opacity={ this.props.opacity } opacity={ this.props.opacity }
/> />

View file

@ -1570,7 +1570,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.getTimelineSets()[0]}
manageReadReceipts={true}
manageReadMarkers={true}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId} highlightedEventId={this.props.highlightedEventId}
eventId={this.props.eventId} eventId={this.props.eventId}

View file

@ -50,9 +50,15 @@ var TimelinePanel = React.createClass({
displayName: 'TimelinePanel', displayName: 'TimelinePanel',
propTypes: { propTypes: {
// The js-sdk Room object for the room whose timeline we are // The js-sdk EventTimelineSet object for the timeline sequence we are
// representing. // representing. This may or may not have a room, depending on what it's
room: React.PropTypes.object.isRequired, // a timeline representing. If it has a room, we maintain RRs etc for
// that room.
timelineSet: React.PropTypes.object.isRequired,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool,
manageReadMarkers: React.PropTypes.bool,
// true to give the component a 'display: none' style. // true to give the component a 'display: none' style.
hidden: React.PropTypes.bool, hidden: React.PropTypes.bool,
@ -101,9 +107,13 @@ var TimelinePanel = React.createClass({
}, },
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 +147,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 +173,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 +185,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 +290,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});
}
} }
}, },
@ -305,7 +317,7 @@ 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 rooms
if (room !== this.props.room) return; if (data.timelineSet !== 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,32 +349,34 @@ 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);
@ -370,7 +384,7 @@ var TimelinePanel = React.createClass({
}, },
onRoomTimelineReset: function(room) { onRoomTimelineReset: function(room) {
if (room !== this.props.room) return; if (room !== this.props.timelineSet.room) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) { if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
this._loadTimeline(); this._loadTimeline();
@ -381,7 +395,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 +406,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 +415,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 +423,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 +476,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 +514,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 +566,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 +595,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 +640,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 +650,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 +711,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 +765,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 +827,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.room.getPendingEvents());
} }
return events; return events;
@ -873,11 +893,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 +907,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;

View file

@ -35,6 +35,7 @@ var USER_ID = '@me:localhost';
describe('TimelinePanel', function() { describe('TimelinePanel', function() {
var sandbox; var sandbox;
var timelineSet;
var room; var room;
var client; var client;
var timeline; var timeline;
@ -60,9 +61,12 @@ describe('TimelinePanel', function() {
timeline = new jssdk.EventTimeline(ROOM_ID); timeline = new jssdk.EventTimeline(ROOM_ID);
room = sinon.createStubInstance(jssdk.Room); room = sinon.createStubInstance(jssdk.Room);
room.getLiveTimeline.returns(timeline);
room.getPendingEvents.returns([]); room.getPendingEvents.returns([]);
timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);
timelineSet.getLiveTimeline.returns(timeline);
timelineSet.room = room;
client = peg.get(); client = peg.get();
client.credentials = {userId: USER_ID}; client.credentials = {userId: USER_ID};
@ -95,7 +99,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 +147,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,
timelineSet: timelineSet,
});
// 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 +185,7 @@ describe('TimelinePanel', function() {
}); });
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room}/>, <TimelinePanel timelineSet={timelineSet}/>,
parentDiv parentDiv
); );
@ -226,7 +233,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