diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c9d3bc732d..df8c850d0d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -48,6 +48,17 @@ module.exports = React.createClass({ ConferenceHandler: React.PropTypes.any }, + /* properties in RoomView objects include: + * + * savedScrollState: the current scroll position in the backlog. Response + * from _calculateScrollState. Updated on scroll events. + * + * savedSearchScrollState: similar to savedScrollState, but specific to the + * search results (we need to preserve savedScrollState when search + * results are visible) + * + * eventNodes: a map from event id to DOM node representing that event + */ getInitialState: function() { var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; return { @@ -207,7 +218,7 @@ module.exports = React.createClass({ if (!toStartOfTimeline && (ev.getSender() !== MatrixClientPeg.get().credentials.userId)) { // update unread count when scrolled up - if (this.savedScrollState.atBottom) { + if (!this.state.searchResults && this.savedScrollState.atBottom) { currentUnread = 0; } else { @@ -331,9 +342,6 @@ module.exports = React.createClass({ // after adding event tiles, we may need to tweak the scroll (either to // keep at the bottom of the timeline, or to maintain the view after // adding events to the top). - - if (this.state.searchResults) return; - this._restoreSavedScrollState(); }, @@ -441,11 +449,16 @@ module.exports = React.createClass({ this.recentEventScroll = undefined; } - if (this.refs.messagePanel && !this.state.searchResults) { - this.savedScrollState = this._calculateScrollState(); - if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState); - if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) { - this.setState({numUnreadMessages: 0}); + if (this.refs.messagePanel) { + if (this.state.searchResults) { + this.savedSearchScrollState = this._calculateScrollState(); + if (DEBUG_SCROLL) console.log("Saved search scroll state", this.savedSearchScrollState); + } else { + this.savedScrollState = this._calculateScrollState(); + if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState); + if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) { + this.setState({numUnreadMessages: 0}); + } } } if (!this.state.paginating && !this.state.searchInProgress) { @@ -510,6 +523,7 @@ module.exports = React.createClass({ searchCount: null, }); + this.savedSearchScrollState = {atBottom: true}; this.nextSearchBatch = null; this._getSearchBatch(term, scope); }, @@ -629,10 +643,18 @@ module.exports = React.createClass({ var result = this.state.searchResults[i]; var mxEv = new Matrix.MatrixEvent(result.result); + if (!EventTile.haveTileForEvent(mxEv)) { + // XXX: can this ever happen? It will make the result count + // not match the displayed count. + continue; + } + + var eventId = mxEv.getId(); + if (self.state.searchScope === 'All') { var roomId = result.result.room_id; if(roomId != lastRoomId) { - ret.push(
  • Room: { cli.getRoom(roomId).name }

  • ); + ret.push(
  • Room: { cli.getRoom(roomId).name }

  • ); lastRoomId = roomId; } } @@ -643,18 +665,16 @@ module.exports = React.createClass({ if (result.context.events_before[0]) { var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]); if (EventTile.haveTileForEvent(mxEv2)) { - ret.push(
  • ); + ret.push(
  • ); } } - if (EventTile.haveTileForEvent(mxEv)) { - ret.push(
  • ); - } + ret.push(
  • ); if (result.context.events_after[0]) { var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]); if (EventTile.haveTileForEvent(mxEv2)) { - ret.push(
  • ); + ret.push(
  • ); } } } @@ -706,15 +726,17 @@ module.exports = React.createClass({ continuation = false; } + var eventId = mxEv.getId(); ret.unshift( -
  • +
  • + +
  • ); if (dateSeparator) { ret.unshift(dateSeparator); } ++count; } - this.lastEventTileCount = count; return ret; }, @@ -884,7 +906,7 @@ module.exports = React.createClass({ }, onCancelClick: function() { - this.setState(this.getInitialState()); + this.setState({editingRoomSettings: false}); }, onLeaveClick: function() { @@ -918,6 +940,13 @@ module.exports = React.createClass({ this.setState({ searching: true }); }, + onCancelSearchClick: function () { + this.setState({ + searching: false, + searchResults: null, + }); + }, + onConferenceNotificationClick: function() { dis.dispatch({ action: 'place_call', @@ -945,12 +974,6 @@ module.exports = React.createClass({ // pixel_offset gives the number of pixels between the bottom of the event // and the bottom of the container. scrollToEvent: function(eventId, pixelOffset) { - var scrollNode = this._getScrollNode(); - if (!scrollNode) return; - - var messageWrapper = this.refs.messagePanel; - if (messageWrapper === undefined) return; - var idx = this._indexForEventId(eventId); if (idx === null) { // we don't seem to have this event in our timeline. Presumably @@ -960,7 +983,7 @@ module.exports = React.createClass({ // // for now, just scroll to the top of the buffer. console.log("Refusing to scroll to unknown event "+eventId); - scrollNode.scrollTop = 0; + this._getScrollNode().scrollTop = 0; return; } @@ -978,14 +1001,88 @@ module.exports = React.createClass({ this.setState({messageCap: minCap}); } - var node = this.eventNodes[eventId]; - if (node === null) { - // getEventTiles should have sorted this out when we set the - // messageCap, so this is weird. - console.error("No node for event, even after rolling back messageCap"); + // the scrollTokens on our DOM nodes are the event IDs, so we can pass + // eventId directly into _scrollToToken. + this._scrollToToken(eventId, pixelOffset); + }, + + _restoreSavedScrollState: function() { + var scrollState = this.state.searchResults ? this.savedSearchScrollState : this.savedScrollState; + if (!scrollState || scrollState.atBottom) { + this.scrollToBottom(); + } else if (scrollState.lastDisplayedScrollToken) { + this._scrollToToken(scrollState.lastDisplayedScrollToken, + scrollState.pixelOffset); + } + }, + + _calculateScrollState: function() { + // we don't save the absolute scroll offset, because that + // would be affected by window width, zoom level, amount of scrollback, + // etc. + // + // instead we save an identifier for the last fully-visible message, + // and the number of pixels the window was scrolled below it - which + // will hopefully be near enough. + // + // Our scroll implementation is agnostic of the precise contents of the + // message list (since it needs to work with both search results and + // timelines). 'refs.messageList' is expected to be a DOM node with a + // number of children, each of which may have a 'data-scroll-token' + // attribute. It is this token which is stored as the + // 'lastDisplayedScrollToken'. + + var messageWrapperScroll = this._getScrollNode(); + // + 1 here to avoid fractional pixel rounding errors + var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1; + + var messageWrapper = this.refs.messagePanel; + var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); + var messages = this.refs.messageList.children; + + for (var i = messages.length-1; i >= 0; --i) { + var node = messages[i]; + if (!node.dataset.scrollToken) continue; + + var boundingRect = node.getBoundingClientRect(); + if (boundingRect.bottom < wrapperRect.bottom) { + return { + atBottom: atBottom, + lastDisplayedScrollToken: node.dataset.scrollToken, + pixelOffset: wrapperRect.bottom - boundingRect.bottom, + } + } + } + + // apparently the entire timeline is below the viewport. Give up. + return { atBottom: true }; + }, + + // scroll the message list to the node with the given scrollToken. See + // notes in _calculateScrollState on how this works. + // + // pixel_offset gives the number of pixels between the bottom of the node + // and the bottom of the container. + _scrollToToken: function(scrollToken, pixelOffset) { + /* find the dom node with the right scrolltoken */ + var node; + var messages = this.refs.messageList.children; + for (var i = messages.length-1; i >= 0; --i) { + var m = messages[i]; + if (!m.dataset.scrollToken) continue; + if (m.dataset.scrollToken == scrollToken) { + node = m; + break; + } + } + + if (!node) { + console.error("No node with scrollToken '"+scrollToken+"'"); return; } + var scrollNode = this._getScrollNode(); + var messageWrapper = this.refs.messagePanel; var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); var boundingRect = node.getBoundingClientRect(); var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; @@ -997,59 +1094,11 @@ module.exports = React.createClass({ } if (DEBUG_SCROLL) { - console.log("Scrolled to event", eventId, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")"); + console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")"); console.log("recentEventScroll now "+this.recentEventScroll); } }, - _restoreSavedScrollState: function() { - var scrollState = this.savedScrollState; - if (scrollState.atBottom) { - this.scrollToBottom(); - } else if (scrollState.lastDisplayedEvent) { - this.scrollToEvent(scrollState.lastDisplayedEvent, - scrollState.pixelOffset); - } - }, - - _calculateScrollState: function() { - // we don't save the absolute scroll offset, because that - // would be affected by window width, zoom level, amount of scrollback, - // etc. - // - // instead we save the id of the last fully-visible event, and the - // number of pixels the window was scrolled below it - which will - // hopefully be near enough. - // - if (this.eventNodes === undefined) return null; - - var messageWrapper = this.refs.messagePanel; - if (messageWrapper === undefined) return null; - var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); - - var messageWrapperScroll = this._getScrollNode(); - // + 1 here to avoid fractional pixel rounding errors - var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1; - - for (var i = this.state.room.timeline.length-1; i >= 0; --i) { - var ev = this.state.room.timeline[i]; - var node = this.eventNodes[ev.getId()]; - if (!node) continue; - - var boundingRect = node.getBoundingClientRect(); - if (boundingRect.bottom < wrapperRect.bottom) { - return { - atBottom: atBottom, - lastDisplayedEvent: ev.getId(), - pixelOffset: wrapperRect.bottom - boundingRect.bottom, - } - } - } - - // apparently the entire timeline is below the viewport. Give up. - return { atBottom: true }; - }, - // get the current scroll position of the room, so that it can be // restored when we switch back to it getScrollState: function() { @@ -1057,11 +1106,17 @@ module.exports = React.createClass({ }, restoreScrollState: function(scrollState) { + if (!this.refs.messagePanel) return; + if(scrollState.atBottom) { // we were at the bottom before. Ideally we'd scroll to the // 'read-up-to' mark here. - } else if (scrollState.lastDisplayedEvent) { - this.scrollToEvent(scrollState.lastDisplayedEvent, + } else if (scrollState.lastDisplayedScrollToken) { + // we might need to backfill, so we call scrollToEvent rather than + // _scrollToToken here. The scrollTokens on our DOM nodes are the + // event IDs, so lastDisplayedScrollToken will be the event ID we need, + // and we can pass it directly into scrollToEvent. + this.scrollToEvent(scrollState.lastDisplayedScrollToken, scrollState.pixelOffset); } }, @@ -1257,7 +1312,7 @@ module.exports = React.createClass({ aux = ; } else if (this.state.searching) { - aux = ; + aux = ; } var conferenceCallNotification = null; @@ -1349,7 +1404,7 @@ module.exports = React.createClass({
    -
      +
      1. {this.getEventTiles()}