2016-02-10 11:03:46 +00:00
|
|
|
/*
|
|
|
|
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: {
|
2016-02-11 15:38:13 +00:00
|
|
|
// true to give the component a 'display: none' style.
|
2016-02-10 11:03:46 +00:00
|
|
|
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() {
|
2016-02-12 13:39:38 +00:00
|
|
|
return this.refs.scrollPanel
|
2016-02-10 11:03:46 +00:00
|
|
|
&& 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();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_getEventTiles: function() {
|
|
|
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
this.eventNodes = {};
|
|
|
|
|
|
|
|
// we do two passes over the events list; first of all, we figure out
|
|
|
|
// which events we want to show, and where the read markers fit into
|
|
|
|
// the list; then we actually create the event tiles. This allows us to
|
|
|
|
// behave slightly differently for the last event in the list.
|
|
|
|
//
|
|
|
|
// (Arguably we could do this when the events are added to this.props,
|
|
|
|
// but that would make it trickier to keep in sync with the read marker, given
|
|
|
|
// the read marker isn't necessarily on an event which we will show).
|
|
|
|
//
|
|
|
|
var eventsToShow = [];
|
|
|
|
|
|
|
|
// the index in 'eventsToShow' of the event *before* which we put the
|
|
|
|
// read marker or its ghost. (Note that it may be equal to
|
|
|
|
// eventsToShow.length, which means it would be at the end of the timeline)
|
|
|
|
var ghostIndex, readMarkerIndex;
|
2016-02-10 11:03:46 +00:00
|
|
|
|
|
|
|
for (var i = 0; i < this.props.events.length; i++) {
|
|
|
|
var mxEv = this.props.events[i];
|
2016-02-12 13:39:38 +00:00
|
|
|
var wantTile = true;
|
2016-02-10 11:03:46 +00:00
|
|
|
|
|
|
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
2016-02-12 13:39:38 +00:00
|
|
|
wantTile = false;
|
2016-02-10 11:03:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.props.isConferenceUser && mxEv.getType() === "m.room.member") {
|
|
|
|
if (this.props.isConferenceUser(mxEv.getSender()) ||
|
|
|
|
this.props.isConferenceUser(mxEv.getStateKey())) {
|
2016-02-12 13:39:38 +00:00
|
|
|
wantTile = false; // suppress conf user join/parts
|
2016-02-10 11:03:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
if (wantTile) {
|
|
|
|
eventsToShow.push(mxEv);
|
2016-02-10 11:03:46 +00:00
|
|
|
}
|
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
var eventId = mxEv.getId();
|
|
|
|
if (eventId == this.props.readMarkerGhostEventId) {
|
|
|
|
ghostIndex = eventsToShow.length;
|
2016-02-10 11:03:46 +00:00
|
|
|
}
|
2016-02-12 13:39:38 +00:00
|
|
|
if (eventId == this.props.readMarkerEventId) {
|
|
|
|
readMarkerIndex = eventsToShow.length;
|
|
|
|
}
|
|
|
|
}
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
var ret = [];
|
|
|
|
|
|
|
|
var prevEvent = null; // the last event we showed
|
|
|
|
|
|
|
|
for (var i = 0; i < eventsToShow.length; i++) {
|
|
|
|
var mxEv = eventsToShow[i];
|
|
|
|
var wantTile = true;
|
|
|
|
|
|
|
|
// insert the read marker if appropriate. Note that doing it here
|
|
|
|
// implicitly means that we never put it at the end of the timeline,
|
|
|
|
// because i will never reach eventsToShow.length.
|
|
|
|
if (i == readMarkerIndex) {
|
|
|
|
// 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 (mxEv.sender && mxEv.sender.userId != this.props.ourUserId) {
|
|
|
|
ret.push(this._getReadMarkerTile());
|
|
|
|
}
|
|
|
|
} else if (i == ghostIndex) {
|
|
|
|
ret.push(this._getReadMarkerGhostTile());
|
2016-02-10 11:03:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var last = false;
|
2016-02-12 13:39:38 +00:00
|
|
|
if (i == eventsToShow.length - 1) {
|
2016-02-10 11:03:46 +00:00
|
|
|
last = true;
|
|
|
|
}
|
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
// add the tiles for this event
|
|
|
|
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
|
|
|
|
prevEvent = mxEv;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
|
|
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
|
|
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
|
|
var ret = [];
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
// is this a continuation of the previous message?
|
|
|
|
var continuation = false;
|
|
|
|
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
|
|
|
&& mxEv.sender.userId === prevEvent.sender.userId
|
|
|
|
&& mxEv.getType() == prevEvent.getType()) {
|
|
|
|
continuation = true;
|
|
|
|
}
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
// 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 = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
|
|
|
ret.push(dateSeparator);
|
|
|
|
continuation = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
|
|
|
<li key={eventId}
|
|
|
|
ref={this._collectEventNode.bind(this, eventId)}
|
2016-02-10 11:03:46 +00:00
|
|
|
data-scroll-token={scrollToken}>
|
|
|
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
|
|
|
last={last} isSelectedEvent={highlight}/>
|
|
|
|
</li>
|
2016-02-12 13:39:38 +00:00
|
|
|
);
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
return ret;
|
|
|
|
},
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
_getReadMarkerTile: function() {
|
|
|
|
var hr;
|
|
|
|
hr = <hr className="mx_RoomView_myReadMarker"
|
|
|
|
style={{opacity: 1, width: '99%'}}
|
|
|
|
/>;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<li key="_readupto"
|
|
|
|
className="mx_RoomView_myReadMarker_container">
|
|
|
|
{hr}
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
},
|
2016-02-10 11:03:46 +00:00
|
|
|
|
2016-02-12 13:39:38 +00:00
|
|
|
_getReadMarkerGhostTile: function() {
|
|
|
|
var hr;
|
|
|
|
hr = <hr className="mx_RoomView_myReadMarker"
|
|
|
|
style={{opacity: 1, width: '99%'}}
|
2016-02-11 15:38:13 +00:00
|
|
|
ref={function(n) {
|
|
|
|
Velocity(n, {opacity: '0', width: '10%'},
|
|
|
|
{duration: 400, easing: 'easeInSine',
|
|
|
|
delay: 1000});
|
|
|
|
}}
|
2016-02-12 13:39:38 +00:00
|
|
|
/>;
|
|
|
|
return (
|
|
|
|
<li key="_readuptoghost"
|
2016-02-11 15:38:13 +00:00
|
|
|
className="mx_RoomView_myReadMarker_container">
|
2016-02-12 13:39:38 +00:00
|
|
|
{hr}
|
|
|
|
</li>
|
|
|
|
);
|
2016-02-10 11:03:46 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_collectEventNode: function(eventId, node) {
|
|
|
|
this.eventNodes[eventId] = node;
|
|
|
|
},
|
2016-02-11 15:38:13 +00:00
|
|
|
|
|
|
|
render: function() {
|
|
|
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
|
|
|
return (
|
|
|
|
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel"
|
|
|
|
onScroll={ this.props.onScroll }
|
|
|
|
onFillRequest={ this.props.onFillRequest }
|
|
|
|
style={ this.props.hidden ? { display: 'none' } : {} }
|
|
|
|
stickyBottom={ this.props.stickyBottom }>
|
|
|
|
{this._getEventTiles()}
|
|
|
|
</ScrollPanel>
|
|
|
|
);
|
|
|
|
},
|
2016-02-10 11:03:46 +00:00
|
|
|
});
|