From ec32347f43b8d90780cb2b5591b8c059508b79bb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Dec 2015 23:04:32 +0000 Subject: [PATCH 1/8] Request more search results when scroll hits top of window --- src/components/structures/RoomView.js | 69 +++++++++++++++++---------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e9341b7389..c9d3bc732d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -346,39 +346,49 @@ module.exports = React.createClass({ // we might not have got enough results from the pagination // request, so give fillSpace() a chance to set off another. - if (!this.fillSpace()) { - this.setState({paginating: false}); + this.setState({paginating: false}); + + if (!this.state.searchResults) { + this.fillSpace(); } }, // check the scroll position, and if we need to, set off a pagination // request. - // - // returns true if a pagination request was started (or is still in progress) fillSpace: function() { if (!this.refs.messagePanel) return; - if (this.state.searchResults) return; // TODO: paginate search results var messageWrapperScroll = this._getScrollNode(); - if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) { - // there's less than a screenful of messages left. Either wind back - // the message cap (if there are enough events in the timeline to - // do so), or fire off a pagination request. - - this.oldScrollHeight = messageWrapperScroll.scrollHeight; - - if (this.state.messageCap < this.state.room.timeline.length) { - var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length); - if (DEBUG_SCROLL) console.log("winding back message cap to", cap); - this.setState({messageCap: cap}); - } else { - var cap = this.state.messageCap + PAGINATE_SIZE; - if (DEBUG_SCROLL) console.log("starting paginate to cap", cap); - this.setState({messageCap: cap, paginating: true}); - MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done(); - return true; - } + if (messageWrapperScroll.scrollTop > messageWrapperScroll.clientHeight) { + return; + } + + // there's less than a screenful of messages left - try to get some + // more messages. + + if (this.state.searchResults) { + if (this.nextSearchBatch) { + if (DEBUG_SCROLL) console.log("requesting more search results"); + this._getSearchBatch(this.state.searchTerm, + this.state.searchScope); + } else { + if (DEBUG_SCROLL) console.log("no more search results"); + } + return; + } + + // 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); + if (DEBUG_SCROLL) console.log("winding back message cap to", cap); + this.setState({messageCap: cap}); + } else if(this.state.room.oldState.paginationToken) { + var cap = this.state.messageCap + PAGINATE_SIZE; + if (DEBUG_SCROLL) console.log("starting paginate to cap", cap); + this.setState({messageCap: cap, paginating: true}); + MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done(); } - return false; }, onResendAllClick: function() { @@ -438,7 +448,9 @@ module.exports = React.createClass({ this.setState({numUnreadMessages: 0}); } } - if (!this.state.paginating) this.fillSpace(); + if (!this.state.paginating && !this.state.searchInProgress) { + this.fillSpace(); + } }, onDragOver: function(ev) { @@ -498,6 +510,7 @@ module.exports = React.createClass({ searchCount: null, }); + this.nextSearchBatch = null; this._getSearchBatch(term, scope); }, @@ -515,8 +528,11 @@ module.exports = React.createClass({ var self = this; - MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope) }) + if (DEBUG_SCROLL) console.log("sending search request"); + MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope), + next_batch: this.nextSearchBatch }) .then(function(data) { + if (DEBUG_SCROLL) console.log("search complete"); if (!self.state.searching || self.searchId != searchId) { console.error("Discarding stale search results"); return; @@ -550,6 +566,7 @@ module.exports = React.createClass({ searchResults: events, searchCount: results.count, }); + self.nextSearchBatch = results.next_batch; }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { From 44c42394aecb4b31431c32c5d21411ad2e0f8e54 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 18 Dec 2015 16:42:46 +0000 Subject: [PATCH 2/8] Fix scroll for search results Make sure that we save scroll position when searching, and use it to preserve offset when backfilling. --- src/components/structures/RoomView.js | 221 ++++++++++++++++---------- 1 file changed, 138 insertions(+), 83 deletions(-) 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()} From fa99c1fc598c0e18aeab1a852d0c540d233be8e4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 17:18:08 +0000 Subject: [PATCH 3/8] make it clear result count is approx --- src/components/views/rooms/RoomHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 1e287ac7dc..0ef6a3becb 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -106,7 +106,7 @@ module.exports = React.createClass({ // don't display the search count until the search completes and // gives us a non-null searchCount. if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) { - searchStatus =
         ({ this.props.searchInfo.searchCount } results)
        ; + searchStatus =
         (~{ this.props.searchInfo.searchCount } results)
        ; } name = From afadb23f89e3dcaf3ce35de2619172c741704542 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 21 Dec 2015 10:46:32 +0000 Subject: [PATCH 4/8] Fix bug with date separator flashing up on scrollback Refactor the event-tile generation loop to go forwards rather than backwards, which makes it easier to figure out whether we are displaying a continuation of the previous event, and whether we need a date separator. Also only display the date separator at the top of the room if there's no more scrollback to be shown. This fixes vector-im/vector-web#431 --- src/components/structures/RoomView.js | 61 +++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e7b97021df..aafc9b8906 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -399,6 +399,12 @@ module.exports = React.createClass({ } }, + // 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; + }, + onResendAllClick: function() { var eventsToResend = this._getUnsentMessages(this.state.room); eventsToResend.forEach(function(event) { @@ -681,7 +687,10 @@ module.exports = React.createClass({ return ret; } - for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) { + + var prevEvent = null; // the last event we showed + var startIdx = Math.max(0, this.state.room.timeline.length-this.state.messageCap); + for (var i = startIdx; i < this.state.room.timeline.length; i++) { var mxEv = this.state.room.timeline[i]; if (!EventTile.haveTileForEvent(mxEv)) { @@ -694,49 +703,45 @@ module.exports = React.createClass({ } } + // is this a continuation of the previous message? var continuation = false; - var last = false; - var dateSeparator = null; - if (i == this.state.room.timeline.length - 1) { - last = true; - } - if (i > 0 && count < this.state.messageCap - 1) { - if (this.state.room.timeline[i].sender && - this.state.room.timeline[i - 1].sender && - (this.state.room.timeline[i].sender.userId === - this.state.room.timeline[i - 1].sender.userId) && - (this.state.room.timeline[i].getType() == - this.state.room.timeline[i - 1].getType()) + if (prevEvent !== null) { + if (mxEv.sender && + prevEvent.sender && + (mxEv.sender.userId === prevEvent.sender.userId) && + (mxEv.getType() == prevEvent.getType()) ) { continuation = true; } - - var ts0 = this.state.room.timeline[i - 1].getTs(); - var ts1 = this.state.room.timeline[i].getTs(); - if (new Date(ts0).toDateString() !== new Date(ts1).toDateString()) { - dateSeparator =
      2. ; - continuation = false; - } } - if (i === 1) { // n.b. 1, not 0, as the 0th event is an m.room.create and so doesn't show on the timeline - var ts1 = this.state.room.timeline[i].getTs(); - dateSeparator =
      3. ; + // do we need a date separator since the last event? + var ts1 = mxEv.getTs(); + if ((prevEvent == null && !this._canPaginate()) || + (prevEvent != null && + new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) { + var dateSeparator =
      4. ; + ret.push(dateSeparator); continuation = false; } + var last = false; + if (i == this.state.room.timeline.length - 1) { + // XXX: we might not show a tile for the last event. + last = true; + } + var eventId = mxEv.getId(); - ret.unshift( + ret.push(
      5. ); - if (dateSeparator) { - ret.unshift(dateSeparator); - } - ++count; + + prevEvent = mxEv; } + return ret; }, From 765e5bdeb163b2be848b03a7112fa7c479e6cd8b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 21 Dec 2015 12:39:10 +0000 Subject: [PATCH 5/8] Add a 'top-of-search' marker Ugly as hell, pending better suggestions. This fixes https://github.com/vector-im/vector-web/issues/547 --- src/components/structures/RoomView.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e7b97021df..3f79e1afe0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -521,6 +521,7 @@ module.exports = React.createClass({ searchResults: [], searchHighlights: [], searchCount: null, + searchCanPaginate: null, }); this.savedSearchScrollState = {atBottom: true}; @@ -579,6 +580,7 @@ module.exports = React.createClass({ searchHighlights: highlights, searchResults: events, searchCount: results.count, + searchCanPaginate: !!(results.next_batch), }); self.nextSearchBatch = results.next_batch; }, function(error) { @@ -639,6 +641,13 @@ module.exports = React.createClass({ var lastRoomId; + if (this.state.searchCanPaginate === false) { + ret.push(
      6. +

        End of search results

        +
      7. + ); + } + for (var i = this.state.searchResults.length - 1; i >= 0; i--) { var result = this.state.searchResults[i]; var mxEv = new Matrix.MatrixEvent(result.result); From 7c285f9ad0ea62f629bb49cc378bbea277341a94 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 21 Dec 2015 13:46:27 +0000 Subject: [PATCH 6/8] Add a 'No results' marker when there are no search results at all. Also reword the 'no more results' marker. --- src/components/structures/RoomView.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3f79e1afe0..d7d3d5988c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -642,10 +642,17 @@ module.exports = React.createClass({ var lastRoomId; if (this.state.searchCanPaginate === false) { - ret.push(
      8. -

        End of search results

        -
      9. - ); + if (this.state.searchResults.length == 0) { + ret.push(
      10. +

        No results

        +
      11. + ); + } else { + ret.push(
      12. +

        No more results

        +
      13. + ); + } } for (var i = this.state.searchResults.length - 1; i >= 0; i--) { From ea2405ab3a18954c3be35002938f2513d4c16120 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 22 Dec 2015 00:57:57 +0000 Subject: [PATCH 7/8] escape double-slash commands --- src/components/views/rooms/MessageComposer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 7c228b5c9d..67fb37d530 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -319,6 +319,9 @@ module.exports = React.createClass({ if (isEmote) { contentText = contentText.substring(4); } + else if (contentText[0] === '/') { + contentText = contentText.substring(1); + } var htmlText; if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) { From 360806a8f1958f52eb92dfdf96829e34d5212a05 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 22 Dec 2015 11:04:39 +0000 Subject: [PATCH 8/8] RoomView: add whitespace for disambiguation --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index aafc9b8906..0f02085638 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -689,7 +689,7 @@ module.exports = React.createClass({ var prevEvent = null; // the last event we showed - var startIdx = Math.max(0, this.state.room.timeline.length-this.state.messageCap); + var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap); for (var i = startIdx; i < this.state.room.timeline.length; i++) { var mxEv = this.state.room.timeline[i];