Merge branch 'rav/roomview_works' into develop
Implementation of new read-marker semantics (PR #160).
This commit is contained in:
parent
1959b03104
commit
10b55036f9
6 changed files with 359 additions and 63 deletions
|
@ -89,6 +89,7 @@ module.exports.components['views.rooms.RoomTile'] = require('./components/views/
|
|||
module.exports.components['views.rooms.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList');
|
||||
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
||||
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
|
||||
module.exports.components['views.rooms.TopUnreadMessagesBar'] = require('./components/views/rooms/TopUnreadMessagesBar');
|
||||
module.exports.components['views.rooms.UserTile'] = require('./components/views/rooms/UserTile');
|
||||
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var sdk = require('../../index');
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
|
@ -35,6 +36,9 @@ module.exports = React.createClass({
|
|||
// event after which we should show a read marker
|
||||
readMarkerEventId: React.PropTypes.string,
|
||||
|
||||
// whether the read marker should be visible
|
||||
readMarkerVisible: React.PropTypes.bool,
|
||||
|
||||
// the userid of our user. This is used to suppress the read marker
|
||||
// for pending messages.
|
||||
ourUserId: React.PropTypes.string,
|
||||
|
@ -91,6 +95,34 @@ module.exports = React.createClass({
|
|||
return this.refs.scrollPanel.getScrollState();
|
||||
},
|
||||
|
||||
// returns one of:
|
||||
//
|
||||
// null: there is no read marker
|
||||
// -1: read marker is above the window
|
||||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
var readMarker = this.refs.readMarkerNode;
|
||||
var messageWrapper = this.refs.scrollPanel;
|
||||
|
||||
if (!readMarker || !messageWrapper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
var readMarkerRect = readMarker.getBoundingClientRect();
|
||||
|
||||
// the read-marker pretends to have zero height when it is actually
|
||||
// two pixels high; +2 here to account for that.
|
||||
if (readMarkerRect.bottom + 2 < wrapperRect.top) {
|
||||
return -1;
|
||||
} else if (readMarkerRect.top < wrapperRect.bottom) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
|
||||
/* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom: function() {
|
||||
|
@ -103,7 +135,7 @@ module.exports = React.createClass({
|
|||
*
|
||||
* 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.
|
||||
* 1/3 of the way down of the container.
|
||||
*/
|
||||
scrollToEvent: function(eventId, pixelOffset) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -166,15 +198,17 @@ module.exports = React.createClass({
|
|||
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
||||
}
|
||||
|
||||
if (eventId == this.props.readMarkerEventId && i < lastShownEventIndex) {
|
||||
// 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.
|
||||
var nextEvent = this.props.events[i+1];
|
||||
if (nextEvent.sender && nextEvent.sender.userId != this.props.ourUserId) {
|
||||
ret.push(this._getReadMarkerTile());
|
||||
readMarkerVisible = true;
|
||||
if (eventId == this.props.readMarkerEventId) {
|
||||
var visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline, we don't want
|
||||
// to show it, but we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't confused.
|
||||
if (i >= lastShownEventIndex) {
|
||||
visible = false;
|
||||
}
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
readMarkerVisible = visible;
|
||||
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
|
@ -234,14 +268,16 @@ module.exports = React.createClass({
|
|||
return ret;
|
||||
},
|
||||
|
||||
_getReadMarkerTile: function() {
|
||||
_getReadMarkerTile: function(visible) {
|
||||
var hr;
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key="_readupto"
|
||||
<li key="_readupto" ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{hr}
|
||||
</li>
|
||||
|
|
|
@ -64,8 +64,8 @@ module.exports = React.createClass({
|
|||
eventId: React.PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
// bottom of the viewport. If not given, will try to put the event in the
|
||||
// middle of the viewprt.
|
||||
// bottom of the viewport. If not given, will try to put the event
|
||||
// 1/3 of the way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
|
@ -94,6 +94,8 @@ module.exports = React.createClass({
|
|||
// 'scroll to bottom' knob, among a couple of other things.
|
||||
atEndOfLiveTimeline: true,
|
||||
|
||||
showTopUnreadMessagesBar: false,
|
||||
|
||||
auxPanelMaxHeight: undefined,
|
||||
}
|
||||
},
|
||||
|
@ -556,6 +558,7 @@ module.exports = React.createClass({
|
|||
atEndOfLiveTimeline: false,
|
||||
});
|
||||
}
|
||||
this._updateTopUnreadMessagesBar();
|
||||
},
|
||||
|
||||
onDragOver: function(ev) {
|
||||
|
@ -879,6 +882,30 @@ module.exports = React.createClass({
|
|||
this.refs.messagePanel.jumpToLiveTimeline();
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
jumpToReadMarker: function() {
|
||||
this.refs.messagePanel.jumpToReadMarker();
|
||||
},
|
||||
|
||||
// update the read marker to match the read-receipt
|
||||
forgetReadMarker: function() {
|
||||
this.refs.messagePanel.forgetReadMarker();
|
||||
},
|
||||
|
||||
// decide whether or not the top 'unread messages' bar should be shown
|
||||
_updateTopUnreadMessagesBar: function() {
|
||||
if (!this.refs.messagePanel)
|
||||
return;
|
||||
|
||||
var pos = this.refs.messagePanel.getReadMarkerPosition();
|
||||
|
||||
// we want to show the bar if the read-marker is off the top of the
|
||||
// screen.
|
||||
var showBar = (pos < 0);
|
||||
|
||||
this.setState({showTopUnreadMessagesBar: showBar});
|
||||
},
|
||||
|
||||
// get the current scroll position of the room, so that it can be
|
||||
// restored when we switch back to it.
|
||||
//
|
||||
|
@ -1247,8 +1274,22 @@ module.exports = React.createClass({
|
|||
eventId={this.props.eventId}
|
||||
eventPixelOffset={this.props.eventPixelOffset}
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||
/>);
|
||||
|
||||
var topUnreadMessagesBar = null;
|
||||
if (this.state.showTopUnreadMessagesBar) {
|
||||
var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
|
||||
topUnreadMessagesBar = (
|
||||
<div className="mx_RoomView_topUnreadMessagesBar">
|
||||
<TopUnreadMessagesBar
|
||||
onScrollUpClick={this.jumpToReadMarker}
|
||||
onCloseClick={this.forgetReadMarker}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
|
@ -1264,6 +1305,7 @@ module.exports = React.createClass({
|
|||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||
} />
|
||||
{ auxPanel }
|
||||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
<div className="mx_RoomView_statusArea">
|
||||
|
|
|
@ -328,13 +328,13 @@ module.exports = React.createClass({
|
|||
|
||||
// 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.
|
||||
// 1/3 of the way down the container.
|
||||
scrollToToken: function(scrollToken, pixelOffset) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
|
||||
// default to the middle
|
||||
// default to 1/3 of the way down
|
||||
if (pixelOffset === undefined) {
|
||||
pixelOffset = scrollNode.clientHeight / 2;
|
||||
pixelOffset = (scrollNode.clientHeight * 2)/ 3;
|
||||
}
|
||||
|
||||
// save the desired scroll state. It's important we do this here rather
|
||||
|
|
|
@ -29,6 +29,11 @@ var PAGINATE_SIZE = 20;
|
|||
var INITIAL_SIZE = 20;
|
||||
var TIMELINE_CAP = 1000; // the most events to show in a timeline
|
||||
|
||||
// consider that the user remains "active" for this many milliseconds after a
|
||||
// user_activity event (and thus don't make the read-marker visible on new
|
||||
// events)
|
||||
var CONSIDER_USER_ACTIVE_FOR_MS = 500;
|
||||
|
||||
var DEBUG = false;
|
||||
|
||||
if (DEBUG) {
|
||||
|
@ -43,7 +48,7 @@ if (DEBUG) {
|
|||
*
|
||||
* Also responsible for handling and sending read receipts.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
var TimelinePanel = React.createClass({
|
||||
displayName: 'TimelinePanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -63,20 +68,40 @@ module.exports = React.createClass({
|
|||
eventId: React.PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
// bottom of the viewport. If not given, will try to put the event in the
|
||||
// middle of the viewprt.
|
||||
// bottom of the viewport. If not given, will try to put the event
|
||||
// 1/3 of the way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
},
|
||||
|
||||
statics: {
|
||||
// a map from room id to read marker event ID
|
||||
roomReadMarkerMap: {},
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
roomReadMarkerTsMap: {},
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var initialReadMarker =
|
||||
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|
||||
|| this._getCurrentReadReceipt();
|
||||
|
||||
return {
|
||||
events: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
canBackPaginate: true,
|
||||
readMarkerEventId: this._getCurrentReadReceipt(),
|
||||
|
||||
// start with the read-marker visible, so that we see its animated
|
||||
// disappearance when swtitching into the room.
|
||||
readMarkerVisible: true,
|
||||
|
||||
readMarkerEventId: initialReadMarker,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -84,9 +109,10 @@ module.exports = React.createClass({
|
|||
debuglog("TimelinePanel: mounting");
|
||||
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this._resetActivityTimer();
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
|
@ -116,7 +142,6 @@ module.exports = React.createClass({
|
|||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
}
|
||||
},
|
||||
|
@ -136,9 +161,26 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMessageListScroll: function () {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
|
||||
// we hide the read marker when it first comes onto the screen, but if
|
||||
// it goes back off the top of the screen (presumably because the user
|
||||
// clicks on the 'jump to bottom' button), we need to re-enable it.
|
||||
if (this.getReadMarkerPosition() < 0) {
|
||||
this.setState({readMarkerVisible: true});
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'user_activity':
|
||||
this._resetActivityTimer();
|
||||
|
||||
// fall-through!
|
||||
|
||||
case 'user_activity_end':
|
||||
// we could treat user_activity_end differently and not
|
||||
// send receipts for messages that have arrived between
|
||||
|
@ -146,10 +188,15 @@ module.exports = React.createClass({
|
|||
// being active, but let's see if this is actually
|
||||
// necessary.
|
||||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_resetActivityTimer: function() {
|
||||
this.user_last_active = Date.now();
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
// ignore events for other rooms
|
||||
if (room !== this.props.room) return;
|
||||
|
@ -158,6 +205,26 @@ module.exports = React.createClass({
|
|||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
||||
// when a new event arrives when the user is not watching the window, but the
|
||||
// window is in its auto-scroll mode, make sure the read marker is visible.
|
||||
//
|
||||
// We consider the user to be watching the window if they performed an action
|
||||
// less than CONSIDER_USER_ACTIVE_FOR_MS ago.
|
||||
//
|
||||
// We ignore events we have sent ourselves; we don't want to see the
|
||||
// read-marker when a remote echo of an event we have just sent takes
|
||||
// more than CONSIDER_USER_ACTIVE_FOR_MS.
|
||||
//
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var sender = ev.sender ? ev.sender.userId : null;
|
||||
var activity_age = Date.now() - this.user_last_active;
|
||||
if (sender != myUserId && this.refs.messagePanel.getScrollState().stuckAtBottom
|
||||
&& activity_age > CONSIDER_USER_ACTIVE_FOR_MS) {
|
||||
this.setState({readMarkerVisible: true});
|
||||
}
|
||||
|
||||
// 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 -
|
||||
|
@ -168,37 +235,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onRoomReceipt: function(receiptEvent, room) {
|
||||
if (room !== this.props.room)
|
||||
return;
|
||||
|
||||
// the received event may or may not be for our user; but it turns out
|
||||
// to be easier to do the processing anyway than to figure out if it
|
||||
// is.
|
||||
var oldReadMarker = this.state.readMarkerEventId;
|
||||
var newReadMarker = this._getCurrentReadReceipt();
|
||||
|
||||
if (newReadMarker == oldReadMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
// suppress the animation when moving forward over an event which was sent
|
||||
// by us; the original RM will have been suppressed so we don't want to show
|
||||
// the animation either.
|
||||
var oldReadMarkerIndex = this._indexForEventId(oldReadMarker);
|
||||
if (oldReadMarkerIndex + 1 < this.state.events.length) {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var nextEvent = this.state.events[oldReadMarkerIndex + 1];
|
||||
if (nextEvent.sender && nextEvent.sender.userId == myUserId) {
|
||||
oldReadMarker = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
readMarkerEventId: newReadMarker,
|
||||
});
|
||||
},
|
||||
|
||||
onRoomRedaction: function(ev, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
|
@ -234,7 +270,9 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
||||
var lastReadEventIndex = this._getLastDisplayedEventIndex({
|
||||
ignoreOwn: true
|
||||
});
|
||||
if (lastReadEventIndex === null) return;
|
||||
|
||||
var lastReadEvent = this.state.events[lastReadEventIndex];
|
||||
|
@ -251,6 +289,45 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
// if the read marker is on the screen, we can now assume we've caught up to the end
|
||||
// of the screen, so move the marker down to the bottom of the screen.
|
||||
updateReadMarker: function() {
|
||||
if (this.getReadMarkerPosition() !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentIndex = this._indexForEventId(this.state.readMarkerEventId);
|
||||
|
||||
// move the RM to *after* the message at the bottom of the screen. This
|
||||
// avoids a problem whereby we never advance the RM if there is a huge
|
||||
// message which doesn't fit on the screen.
|
||||
//
|
||||
// But ignore local echoes for this - they have a temporary event ID
|
||||
// and we'll get confused when their ID changes and we can't figure out
|
||||
// where the RM is pointing to. The read marker will be invisible for
|
||||
// now anyway, so this doesn't really matter.
|
||||
var lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
allowPartial: true,
|
||||
ignoreEchoes: true,
|
||||
});
|
||||
|
||||
if (lastDisplayedIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs());
|
||||
|
||||
// the read-marker should become invisible, so that if the user scrolls
|
||||
// down, they don't see it.
|
||||
if(this.state.readMarkerVisible) {
|
||||
this.setState({
|
||||
readMarkerVisible: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/* jump down to the bottom of this room, where new events are arriving
|
||||
*/
|
||||
jumpToLiveTimeline: function() {
|
||||
|
@ -268,6 +345,35 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/* scroll to show the read-up-to marker
|
||||
*/
|
||||
jumpToReadMarker: function() {
|
||||
if (!this.state.readMarkerEventId)
|
||||
return;
|
||||
if (!this.refs.messagePanel)
|
||||
return;
|
||||
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId);
|
||||
},
|
||||
|
||||
|
||||
/* update the read-up-to marker to match the read receipt
|
||||
*/
|
||||
forgetReadMarker: function() {
|
||||
var rmId = this._getCurrentReadReceipt();
|
||||
|
||||
// see if we know the timestamp for the rr event
|
||||
var tl = this.props.room.getTimelineForEvent(rmId);
|
||||
var rmTs;
|
||||
if (tl) {
|
||||
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
|
||||
if (event) {
|
||||
rmTs = event.getTs();
|
||||
}
|
||||
}
|
||||
|
||||
this._setReadMarker(rmId, rmTs);
|
||||
},
|
||||
|
||||
/* return true if the content is fully scrolled down and we are
|
||||
* at the end of the live timeline.
|
||||
*/
|
||||
|
@ -289,6 +395,33 @@ module.exports = React.createClass({
|
|||
return this.refs.messagePanel.getScrollState();
|
||||
},
|
||||
|
||||
// returns one of:
|
||||
//
|
||||
// null: there is no read marker
|
||||
// -1: read marker is above the window
|
||||
// 0: read marker is visible
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
if (!this.refs.messagePanel) { return null; }
|
||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// the messagePanel doesn't know where the read marker is.
|
||||
// if we know the timestamp of the read marker, make a guess based on that.
|
||||
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId];
|
||||
if (rmTs && this.state.events) {
|
||||
if (rmTs < this.state.events[0].getTs()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
_initTimeline: function(props) {
|
||||
var initialEvent = props.eventId;
|
||||
var pixelOffset = props.eventPixelOffset;
|
||||
|
@ -304,7 +437,7 @@ module.exports = React.createClass({
|
|||
*
|
||||
* @param {number?} pixelOffset offset to position the given event at
|
||||
* (pixels from the bottom of the view). If undefined, will put the
|
||||
* event in the middle of the view.
|
||||
* event 1/3 of the way down the view.
|
||||
*
|
||||
* returns a promise which will resolve when the load completes.
|
||||
*/
|
||||
|
@ -343,6 +476,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -369,16 +503,27 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
},
|
||||
|
||||
_getLastDisplayedEventIndexIgnoringOwn: function() {
|
||||
_getLastDisplayedEventIndex: function(opts) {
|
||||
opts = opts || {};
|
||||
var ignoreOwn = opts.ignoreOwn || false;
|
||||
var ignoreEchoes = opts.ignoreEchoes || false;
|
||||
var allowPartial = opts.allowPartial || false;
|
||||
|
||||
var messagePanel = this.refs.messagePanel;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||
var ev = this.state.events[i];
|
||||
|
||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// local echoes have a fake event ID
|
||||
if (ignoreEchoes && ev.status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -386,8 +531,8 @@ module.exports = React.createClass({
|
|||
if (!node) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
|
||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -412,6 +557,27 @@ module.exports = React.createClass({
|
|||
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
},
|
||||
|
||||
_setReadMarker: function(eventId, eventTs) {
|
||||
if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) {
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
return;
|
||||
}
|
||||
|
||||
// ideally we'd sync these via the server, but for now just stash them
|
||||
// in a map.
|
||||
TimelinePanel.roomReadMarkerMap[this.props.room.roomId] = eventId;
|
||||
|
||||
// in order to later figure out if the read marker is
|
||||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs;
|
||||
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState({
|
||||
readMarkerEventId: eventId,
|
||||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
|
@ -452,12 +618,15 @@ module.exports = React.createClass({
|
|||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.props.onScroll }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = TimelinePanel;
|
||||
|
|
48
src/components/views/rooms/TopUnreadMessagesBar.js
Normal file
48
src/components/views/rooms/TopUnreadMessagesBar.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TopUnreadMessagesBar',
|
||||
|
||||
propTypes: {
|
||||
onScrollUpClick: React.PropTypes.func,
|
||||
onCloseClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_TopUnreadMessagesBar">
|
||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
<img src="img/scrollup.svg" width="24" height="24"
|
||||
alt="Scroll to unread messages"
|
||||
title="Scroll to unread messages"/>
|
||||
Unread messages
|
||||
</div>
|
||||
<img className="mx_TopUnreadMessagesBar_close"
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
alt="Close" title="Close"
|
||||
onClick={this.props.onCloseClick} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in a new issue