From c3692378fabe2a5c4eb651dcb40d33ffb5b4e5a6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 10 Feb 2016 11:03:46 +0000 Subject: [PATCH] Factor a stateless messagepanel out from RoomView --- src/component-index.js | 1 + src/components/structures/MessagePanel.js | 264 ++++++++++++++++++++++ src/components/structures/RoomView.js | 180 ++------------- 3 files changed, 287 insertions(+), 158 deletions(-) create mode 100644 src/components/structures/MessagePanel.js diff --git a/src/component-index.js b/src/component-index.js index 5a37a98913..a0abd33348 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -31,6 +31,7 @@ module.exports.components['structures.login.Login'] = require('./components/stru module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); +module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar'); module.exports.components['structures.RoomView'] = require('./components/structures/RoomView'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js new file mode 100644 index 0000000000..d0c12eac31 --- /dev/null +++ b/src/components/structures/MessagePanel.js @@ -0,0 +1,264 @@ +/* +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 sdk = require('../../index'); + +/* stateless UI component which builds the event tiles in the room timeline. + */ +module.exports = React.createClass({ + displayName: 'MessagePanel', + + propTypes: { + // true to give the component a 'display: hidden' style. + hidden: React.PropTypes.bool, + + // the list of MatrixEvents to display + events: React.PropTypes.array.isRequired, + + // ID of an event to highlight. If undefined, no event will be highlighted. + highlightedEventId: React.PropTypes.string, + + // event after which we should show a read marker + readMarkerEventId: React.PropTypes.string, + + // event after which we should show an animating disappearance of a + // read marker + readMarkerGhostEventId: React.PropTypes.string, + + // the userid of our user. This is used to suppress the read marker + // for pending messages. + ourUserId: React.PropTypes.string, + + // true to suppress the date at the start of the timeline + suppressFirstDateSeparator: React.PropTypes.bool, + + // 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 + // for more details. + stickyBottom: React.PropTypes.bool, + + // callback to determine if a user is the magic freeswitch conference + // user. Takes one parameter, which is a user id. Should return true if + // the user is the conference user. + isConferenceUser: React.PropTypes.func, + + // callback which is called when the panel is scrolled. + onScroll: React.PropTypes.func, + + // callback which is called when more content is needed. + onFillRequest: React.PropTypes.func, + }, + + /* get the DOM node representing the given event */ + getNodeForEventId: function(eventId) { + if (!this.eventNodes) { + return undefined; + } + + return this.eventNodes[eventId]; + }, + + /* return true if the content is fully scrolled down right now; else false. + */ + isAtBottom: function() { + return this.refs.scrollPanel + && this.refs.scrollPanel.isAtBottom(); + }, + + /* get the current scroll state. See ScrollPanel.getScrollState for + * details. + * + * returns null if we are not mounted. + */ + getScrollState: function() { + if (!this.refs.scrollPanel) { return null; } + return this.refs.scrollPanel.getScrollState(); + }, + + /* jump to the bottom of the content. + */ + scrollToBottom: function() { + if (this.refs.scrollPanel) { + this.refs.scrollPanel.scrollToBottom(); + } + }, + + /* jump to the given event id. + * + * pixelOffset gives the number of pixels between the bottom of the node + * and the bottom of the container. If undefined, it will put the node + * in the middle of the container. + */ + scrollToEvent: function(eventId, pixelOffset) { + if (this.refs.scrollPanel) { + this.refs.scrollPanel.scrollToToken(eventId, pixelOffset); + } + }, + + /* check the scroll state and send out pagination requests if necessary. + */ + checkFillState: function() { + if (this.refs.scrollPanel) { + this.refs.scrollPanel.checkFillState(); + } + }, + + render: function() { + var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + return ( + + {this._getEventTiles()} + + ); + }, + + _getEventTiles: function() { + var DateSeparator = sdk.getComponent('messages.DateSeparator'); + var EventTile = sdk.getComponent('rooms.EventTile'); + + var ret = []; + + var prevEvent = null; // the last event we showed + var ghostIndex; + var readMarkerIndex; + for (var i = 0; i < this.props.events.length; i++) { + var mxEv = this.props.events[i]; + + if (!EventTile.haveTileForEvent(mxEv)) { + continue; + } + + if (this.props.isConferenceUser && mxEv.getType() === "m.room.member") { + if (this.props.isConferenceUser(mxEv.getSender()) || + this.props.isConferenceUser(mxEv.getStateKey())) { + continue; // suppress conf user join/parts + } + } + + // now we've decided whether or not to show this message, + // add the read up to marker if appropriate + // doing this here means we implicitly do not show the marker + // if it's at the bottom + // NB. it would be better to decide where the read marker was going + // when the state changed rather than here in the render method, but + // this is where we decide what messages we show so it's the only + // place we know whether we're at the bottom or not. + var mxEvSender = mxEv.sender ? mxEv.sender.userId : null; + if (prevEvent && prevEvent.getId() == this.props.readMarkerEventId) { + // suppress the read marker if the next event is sent by us; this + // is a nonsensical and temporary situation caused by the delay between + // us sending a message and receiving the synthesized receipt. + if (mxEvSender != this.props.ourUserId) { + var hr; + hr = ( +
); + readMarkerIndex = ret.length; + ret.push( +
  • + {hr} +
  • ); + } + } + + // is this a continuation of the previous message? + var continuation = false; + if (prevEvent !== null) { + if (mxEvSender && + prevEvent.sender && + (mxEvSender === prevEvent.sender.userId) && + (mxEv.getType() == prevEvent.getType()) + ) + { + continuation = true; + } + } + + // do we need a date separator since the last event? + var ts1 = mxEv.getTs(); + if ((prevEvent == null && !this.props.suppressFirstDateSeparator) || + (prevEvent != null && + new Date(prevEvent.getTs()).toDateString() + !== new Date(ts1).toDateString())) { + var dateSeparator =
  • ; + ret.push(dateSeparator); + continuation = false; + } + + var last = false; + if (i == this.props.events.length - 1) { + // XXX: we might not show a tile for the last event. + last = true; + } + + var eventId = mxEv.getId(); + var highlight = (eventId == this.props.highlightedEventId); + + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". + var scrollToken = mxEv.status ? undefined : eventId; + + ret.push( +
  • + +
  • + ); + + // A read up to marker has died and returned as a ghost! + // Lives in the dom as the ghost of the previous one while it fades away + if (eventId == this.props.readMarkerGhostEventId) { + ghostIndex = ret.length; + } + + prevEvent = mxEv; + } + + // splice the read marker ghost in now that we know whether the read receipt + // is the last element or not, because we only decide as we're going along. + if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) { + var hr; + hr = (
    ); + ret.splice(ghostIndex, 0, ( +
  • + {hr} +
  • + )); + } + + return ret; + }, + + _collectEventNode: function(eventId, node) { + if (this.eventNodes == undefined) this.eventNodes = {}; + this.eventNodes[eventId] = node; + }, +}); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d68ae35dc8..afe3a5112f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -19,7 +19,6 @@ limitations under the License. // - Search results component // - Drag and drop // - File uploading - uploadFile() -// - Timeline component (alllll the logic in getEventTiles()) var React = require("react"); var ReactDOM = require("react-dom"); @@ -77,10 +76,6 @@ module.exports = React.createClass({ highlightedEventId: React.PropTypes.string, }, - /* properties in RoomView objects include: - * - * 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 { @@ -227,7 +222,7 @@ module.exports = React.createClass({ return; } if (eventId) { - this.refs.messagePanel.scrollToToken(eventId, pixelOffset); + this.refs.messagePanel.scrollToEvent(eventId, pixelOffset); } else { this.refs.messagePanel.scrollToBottom(); } @@ -537,10 +532,6 @@ module.exports = React.createClass({ }, componentDidMount: function() { - if (this.refs.messagePanel) { - this._initialiseMessagePanel(); - } - var call = CallHandler.getCallForRoom(this.props.roomId); var callState = call ? call.call_state : "ended"; this.setState({ @@ -585,23 +576,6 @@ module.exports = React.createClass({ ); }, 500), - _initialiseMessagePanel: function() { - var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); - this.refs.messagePanel.initialised = true; - this.updateTint(); - }, - - componentDidUpdate: function() { - // we need to initialise the messagepanel if we've just joined the - // room. TODO: we really really ought to factor out messagepanel to a - // separate component to avoid this ridiculous dance. - if (!this.refs.messagePanel) return; - - if (!this.refs.messagePanel.initialised) { - this._initialiseMessagePanel(); - } - }, - _onTimelineUpdated: function(gotResults) { // we might have switched rooms since the load started - just bin // the results if so. @@ -954,125 +928,6 @@ module.exports = React.createClass({ return ret; }, - getEventTiles: function() { - var DateSeparator = sdk.getComponent('messages.DateSeparator'); - - var ret = []; - var count = 0; - - var EventTile = sdk.getComponent('rooms.EventTile'); - - var prevEvent = null; // the last event we showed - var ghostIndex; - var readMarkerIndex; - for (var i = 0; i < this.state.events.length; i++) { - var mxEv = this.state.events[i]; - - if (!EventTile.haveTileForEvent(mxEv)) { - continue; - } - if (this.props.ConferenceHandler && mxEv.getType() === "m.room.member") { - if (this.props.ConferenceHandler.isConferenceUser(mxEv.getSender()) || - this.props.ConferenceHandler.isConferenceUser(mxEv.getStateKey())) { - continue; // suppress conf user join/parts - } - } - - // now we've decided whether or not to show this message, - // add the read up to marker if appropriate - // doing this here means we implicitly do not show the marker - // if it's at the bottom - // NB. it would be better to decide where the read marker was going - // when the state changed rather than here in the render method, but - // this is where we decide what messages we show so it's the only - // place we know whether we're at the bottom or not. - var self = this; - var mxEvSender = mxEv.sender ? mxEv.sender.userId : null; - if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) { - var hr; - hr = (
    ); - readMarkerIndex = ret.length; - ret.push(
  • {hr}
  • ); - } - - // is this a continuation of the previous message? - var continuation = false; - if (prevEvent !== null) { - if (mxEv.sender && - prevEvent.sender && - (mxEv.sender.userId === prevEvent.sender.userId) && - (mxEv.getType() == prevEvent.getType()) - ) - { - continuation = true; - } - } - - // do we need a date separator since the last event? - var ts1 = mxEv.getTs(); - if ((prevEvent == null && !this.state.canBackPaginate) || - (prevEvent != null && - new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) { - var dateSeparator =
  • ; - ret.push(dateSeparator); - continuation = false; - } - - var last = false; - if (i == this.state.events.length - 1) { - // XXX: we might not show a tile for the last event. - last = true; - } - - var eventId = mxEv.getId(); - var highlight = (eventId == this.props.highlightedEventId); - - // we can't use local echoes as scroll tokens, because their event IDs change. - // Local echos have a send "status". - var scrollToken = mxEv.status ? undefined : eventId; - - ret.push( -
  • - -
  • - ); - - // A read up to marker has died and returned as a ghost! - // Lives in the dom as the ghost of the previous one while it fades away - if (eventId == this.state.readMarkerGhostEventId) { - ghostIndex = ret.length; - } - - prevEvent = mxEv; - } - - // splice the read marker ghost in now that we know whether the read receipt - // is the last element or not, because we only decide as we're going along. - if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) { - var hr; - hr = (
    ); - ret.splice(ghostIndex, 0, ( -
  • {hr}
  • - )); - } - - return ret; - }, - - _collectEventNode: function(eventId, node) { - if (this.eventNodes == undefined) this.eventNodes = {}; - this.eventNodes[eventId] = node; - }, - _indexForEventId(evId) { for (var i = 0; i < this.state.events.length; ++i) { if (evId == this.state.events[i].getId()) { @@ -1130,11 +985,10 @@ module.exports = React.createClass({ }, _getLastDisplayedEventIndexIgnoringOwn: function() { - if (this.eventNodes === undefined) return null; + var messagePanel = this.refs.messagePanel; + if (messagePanel === undefined) return null; - var messageWrapper = this.refs.messagePanel; - if (messageWrapper === undefined) return null; - var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); + var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect(); for (var i = this.state.events.length-1; i >= 0; --i) { var ev = this.state.events[i]; @@ -1143,7 +997,7 @@ module.exports = React.createClass({ continue; } - var node = this.eventNodes[ev.getId()]; + var node = messagePanel.getNodeForEventId(ev.getId()); if (!node) continue; var boundingRect = node.getBoundingClientRect(); @@ -1412,10 +1266,10 @@ module.exports = React.createClass({ var CallView = sdk.getComponent("voip.CallView"); var RoomSettings = sdk.getComponent("rooms.RoomSettings"); var SearchBar = sdk.getComponent("rooms.SearchBar"); - var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); var TintableSvg = sdk.getComponent("elements.TintableSvg"); var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); var Loader = sdk.getComponent("elements.Spinner"); + var MessagePanel = sdk.getComponent("structures.MessagePanel"); if (!this._timelineWindow) { if (this.props.roomId) { @@ -1687,14 +1541,24 @@ module.exports = React.createClass({ var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); messagePanel = ( - { + this.refs.messagePanel = r; + this.updateTint(); + }} + hidden={ hideMessagePanel } + events={ this.state.events } + highlightedEventId={ this.props.highlightedEventId } + readMarkerEventId={ this.state.readMarkerEventId } + readMarkerGhostEventId={ this.state.readMarkerGhostEventId } + ourUserId={ MatrixClientPeg.get().credentials.userId } + suppressFirstDateSeparator={ this.state.canBackPaginate } + stickyBottom={ stickyBottom } + isConferenceUser={this.props.ConferenceHandler ? + this.props.ConferenceHandler.isConferenceUser : + null } onScroll={ this.onMessageListScroll } onFillRequest={ this.onMessageListFillRequest } - style={ hideMessagePanel ? { display: 'none' } : {} } - stickyBottom={ stickyBottom }> -
  • - {this.getEventTiles()} -
    + /> ); }