diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index dbb3dbf83e..bd8aa31a6f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -39,7 +39,8 @@ function createClient(hs_url, is_url, user_id, access_token, guestAccess) { baseUrl: hs_url, idBaseUrl: is_url, accessToken: access_token, - userId: user_id + userId: user_id, + timelineSupport: true, }; if (localStorage) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6050076d12..7713782252 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -470,10 +470,11 @@ module.exports = React.createClass({ newState.ready = true; } this.setState(newState); + /* if (this.scrollStateMap[roomId]) { var scrollState = this.scrollStateMap[roomId]; this.refs.roomView.restoreScrollState(scrollState); - } + }*/ if (this.refs.roomView && showSettings) { this.refs.roomView.showSettings(true); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 37866ca646..5dbdb1c56d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,6 +26,7 @@ var ReactDOM = require("react-dom"); var q = require("q"); var classNames = require("classnames"); var Matrix = require("matrix-js-sdk"); +var EventTimeline = Matrix.EventTimeline; var MatrixClientPeg = require("../../MatrixClientPeg"); var ContentMessages = require("../../ContentMessages"); @@ -44,6 +45,7 @@ var Tinter = require("../../Tinter"); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 20; var SEND_READ_RECEIPT_DELAY = 2000; +var TIMELINE_CAP = 1000; // the most events to show in a timeline var DEBUG_SCROLL = false; @@ -70,7 +72,9 @@ module.exports = React.createClass({ var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; return { room: room, - messageCap: INITIAL_SIZE, + events: [], + canBackPaginate: true, + paginating: room != null, editingRoomSettings: false, uploadingRoomSettings: false, numUnreadMessages: 0, @@ -80,7 +84,7 @@ module.exports = React.createClass({ syncState: MatrixClientPeg.get().getSyncState(), hasUnsentMessages: this._hasUnsentMessages(room), callState: null, - autoPeekDone: false, // track whether our autoPeek (if any) has completed) + timelineLoaded: false, // track whether our room timeline has loaded guestsCanJoin: false, canPeek: false, readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null, @@ -92,7 +96,6 @@ module.exports = React.createClass({ componentWillMount: function() { this.last_rr_sent_event_id = undefined; this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onNewRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); @@ -110,6 +113,15 @@ module.exports = React.createClass({ this.forceUpdate(); } }); + + + // to make the timeline load work correctly, build up a chain of promises which + // take us through the necessary steps. + + // First of all, we may need to load the room. Construct a promise + // which resolves to the Room object. + var roomProm; + // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -118,22 +130,43 @@ module.exports = React.createClass({ // We can /peek though. If it fails then we present the join UI. If it // succeeds then great, show the preview (but we still may be able to /join!). if (!this.state.room) { - if (this.props.autoPeek) { - console.log("Attempting to peek into room %s", this.props.roomId); - MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => { - console.error("Failed to peek into room: %s", err); - }).finally(() => { - // we don't need to do anything - JS SDK will emit Room events - // which will update the UI. - this.setState({ - autoPeekDone: true - }); - }); + if (!this.props.autoPeek) { + console.log("No room loaded, and autopeek disabled"); + return; } + + console.log("Attempting to peek into room %s", this.props.roomId); + + roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => { + console.error("Failed to peek into room: %s", err); + throw err; + }).then((room) => { + this.setState({ + room: room + }); + return room; + }); + } else { + roomProm = q(this.state.room); } - else { - this._calculatePeekRules(this.state.room); - } + + // Next, load the timeline. + roomProm.then((room) => { + this._calculatePeekRules(room); + this._timelineWindow = new Matrix.TimelineWindow( + MatrixClientPeg.get(), room, + {windowLimit: TIMELINE_CAP}); + + return this._timelineWindow.load(undefined, + INITIAL_SIZE); + }).then(() => { + debuglog("RoomView: timeline loaded"); + this._onTimelineUpdated(true); + }).finally(() => { + this.setState({ + timelineLoaded: true + }); + }).done(); }, componentWillUnmount: function() { @@ -156,7 +189,6 @@ module.exports = React.createClass({ } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room", this.onNewRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); @@ -248,46 +280,40 @@ module.exports = React.createClass({ /*componentWillReceiveProps: function(props) { },*/ - onRoomTimeline: function(ev, room, toStartOfTimeline) { + onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { if (this.unmounted) return; - // ignore anything that comes in whilst paginating: we get one - // event for each new matrix event so this would cause a huge - // number of UI updates. Just update the UI when the paginate - // call returns. - if (this.state.paginating) return; + // ignore events for other rooms + if (room.roomId != this.props.roomId) return; + + // ignore anything but real-time updates at the end of the room: + // updates from pagination will happen when the paginate completes. + if (toStartOfTimeline || !data || !data.liveEvent) return; // no point handling anything while we're waiting for the join to finish: // we'll only be showing a spinner. if (this.state.joining) return; - if (room.roomId != this.props.roomId) return; - var currentUnread = this.state.numUnreadMessages; - if (!toStartOfTimeline && - (ev.getSender() !== MatrixClientPeg.get().credentials.userId)) { + if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) { // update unread count when scrolled up if (!this.state.searchResults && this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) { - currentUnread = 0; + // no change } else { - currentUnread += 1; + this.setState((state, props) => { + return {numUnreadMessages: state.numUnreadMessages + 1}; + }); } } - this.setState({ - room: MatrixClientPeg.get().getRoom(this.props.roomId), - numUnreadMessages: currentUnread - }); - }, - - onNewRoom: function(room) { - if (room.roomId == this.props.roomId) { - this.setState({ - room: room - }); + // tell the messagepanel to go paginate itself. This in turn will cause + // onMessageListFillRequest to be called, which will call + // _onTimelineUpdated, which will update the state with the new event - + // so there is no need update the state here. + // + if (this.refs.messagePanel) { + this.refs.messagePanel.checkFillState(); } - - this._calculatePeekRules(room); }, _calculatePeekRules: function(room) { @@ -349,14 +375,14 @@ module.exports = React.createClass({ // if the event after the one referenced in the read receipt if sent by us, do nothing since // this is a temporary period before the synthesized receipt for our own message arrives var readMarkerGhostEventIndex; - for (var i = 0; i < room.timeline.length; ++i) { - if (room.timeline[i].getId() == readMarkerGhostEventId) { + for (var i = 0; i < this.state.events.length; ++i) { + if (this.state.events[i].getId() == readMarkerGhostEventId) { readMarkerGhostEventIndex = i; break; } } - if (readMarkerGhostEventIndex + 1 < room.timeline.length) { - var nextEvent = room.timeline[readMarkerGhostEventIndex + 1]; + if (readMarkerGhostEventIndex + 1 < this.state.events.length) { + var nextEvent = this.state.events[readMarkerGhostEventIndex + 1]; if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) { readMarkerGhostEventId = undefined; } @@ -504,17 +530,21 @@ module.exports = React.createClass({ } }, - _paginateCompleted: function() { - debuglog("paginate complete"); - - // we might have switched rooms since the paginate started - just bin + _onTimelineUpdated: function(gotResults) { + // we might have switched rooms since the load started - just bin // the results if so. if (this.unmounted) return; this.setState({ - room: MatrixClientPeg.get().getRoom(this.props.roomId), paginating: false, }); + + if (gotResults) { + this.setState({ + events: this._timelineWindow.getEvents(), + canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS), + }); + } }, onSearchResultsFillRequest: function(backwards) { @@ -534,30 +564,19 @@ module.exports = React.createClass({ // set off a pagination request. onMessageListFillRequest: function(backwards) { - if (!backwards) + var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; + if(!this._timelineWindow.canPaginate(dir)) { + debuglog("RoomView: can't paginate at this time; backwards:"+backwards); return q(false); - - // Either wind back the message cap (if there are enough events in the - // timeline to do so), or fire off a pagination request. - - if (this.state.messageCap < this.state.room.timeline.length) { - var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length); - debuglog("winding back message cap to", cap); - this.setState({messageCap: cap}); - return q(true); - } else if(this.state.room.oldState.paginationToken) { - var cap = this.state.messageCap + PAGINATE_SIZE; - debuglog("starting paginate to cap", cap); - this.setState({messageCap: cap, paginating: true}); - return MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE). - finally(this._paginateCompleted).then(true); } - }, + this.setState({paginating: true}); - // return true if there's more messages in the backlog which we aren't displaying - _canPaginate: function() { - return (this.state.messageCap < this.state.room.timeline.length) || - this.state.room.oldState.paginationToken; + debuglog("RoomView: Initiating paginate; backwards:"+backwards); + return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => { + debuglog("RoomView: paginate complete backwards:"+backwards+"; success:"+r); + this._onTimelineUpdated(r); + return r; + }); }, onResendAllClick: function() { @@ -829,11 +848,10 @@ module.exports = React.createClass({ var EventTile = sdk.getComponent('rooms.EventTile'); var prevEvent = null; // the last event we showed - var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap); - var readMarkerIndex; var ghostIndex; - for (var i = startIdx; i < this.state.room.timeline.length; i++) { - var mxEv = this.state.room.timeline[i]; + var readMarkerIndex; + for (var i = 0; i < this.state.events.length; i++) { + var mxEv = this.state.events[i]; if (!EventTile.haveTileForEvent(mxEv)) { continue; @@ -879,7 +897,7 @@ module.exports = React.createClass({ // do we need a date separator since the last event? var ts1 = mxEv.getTs(); - if ((prevEvent == null && !this._canPaginate()) || + if ((prevEvent == null && !this.state.canBackPaginate) || (prevEvent != null && new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) { var dateSeparator =