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.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList');
|
||||||
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
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.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.rooms.UserTile'] = require('./components/views/rooms/UserTile');
|
||||||
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
var ReactDOM = require("react-dom");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
|
|
||||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
/* (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
|
// event after which we should show a read marker
|
||||||
readMarkerEventId: React.PropTypes.string,
|
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
|
// the userid of our user. This is used to suppress the read marker
|
||||||
// for pending messages.
|
// for pending messages.
|
||||||
ourUserId: React.PropTypes.string,
|
ourUserId: React.PropTypes.string,
|
||||||
|
@ -91,6 +95,34 @@ module.exports = React.createClass({
|
||||||
return this.refs.scrollPanel.getScrollState();
|
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.
|
/* jump to the bottom of the content.
|
||||||
*/
|
*/
|
||||||
scrollToBottom: function() {
|
scrollToBottom: function() {
|
||||||
|
@ -103,7 +135,7 @@ module.exports = React.createClass({
|
||||||
*
|
*
|
||||||
* pixelOffset gives the number of pixels between the bottom of the node
|
* 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
|
* 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) {
|
scrollToEvent: function(eventId, pixelOffset) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this.refs.scrollPanel) {
|
||||||
|
@ -166,15 +198,17 @@ module.exports = React.createClass({
|
||||||
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventId == this.props.readMarkerEventId && i < lastShownEventIndex) {
|
if (eventId == this.props.readMarkerEventId) {
|
||||||
// suppress the read marker if the next event is sent by us; this
|
var visible = this.props.readMarkerVisible;
|
||||||
// is a nonsensical and temporary situation caused by the delay between
|
|
||||||
// us sending a message and receiving the synthesized receipt.
|
// if the read marker comes at the end of the timeline, we don't want
|
||||||
var nextEvent = this.props.events[i+1];
|
// to show it, but we still want to create the <li/> for it so that the
|
||||||
if (nextEvent.sender && nextEvent.sender.userId != this.props.ourUserId) {
|
// algorithms which depend on its position on the screen aren't confused.
|
||||||
ret.push(this._getReadMarkerTile());
|
if (i >= lastShownEventIndex) {
|
||||||
readMarkerVisible = true;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
readMarkerVisible = visible;
|
||||||
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
||||||
// there is currently a read-up-to marker at this point, but no
|
// there is currently a read-up-to marker at this point, but no
|
||||||
// more. Show an animation of it disappearing.
|
// more. Show an animation of it disappearing.
|
||||||
|
@ -234,14 +268,16 @@ module.exports = React.createClass({
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getReadMarkerTile: function() {
|
_getReadMarkerTile: function(visible) {
|
||||||
var hr;
|
var hr;
|
||||||
|
if (visible) {
|
||||||
hr = <hr className="mx_RoomView_myReadMarker"
|
hr = <hr className="mx_RoomView_myReadMarker"
|
||||||
style={{opacity: 1, width: '99%'}}
|
style={{opacity: 1, width: '99%'}}
|
||||||
/>;
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key="_readupto"
|
<li key="_readupto" ref="readMarkerNode"
|
||||||
className="mx_RoomView_myReadMarker_container">
|
className="mx_RoomView_myReadMarker_container">
|
||||||
{hr}
|
{hr}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -64,8 +64,8 @@ module.exports = React.createClass({
|
||||||
eventId: React.PropTypes.string,
|
eventId: React.PropTypes.string,
|
||||||
|
|
||||||
// where to position the event given by eventId, in pixels from the
|
// 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
|
// bottom of the viewport. If not given, will try to put the event
|
||||||
// middle of the viewprt.
|
// 1/3 of the way down the viewport.
|
||||||
eventPixelOffset: React.PropTypes.number,
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
// 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.
|
// 'scroll to bottom' knob, among a couple of other things.
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
|
||||||
|
showTopUnreadMessagesBar: false,
|
||||||
|
|
||||||
auxPanelMaxHeight: undefined,
|
auxPanelMaxHeight: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -556,6 +558,7 @@ module.exports = React.createClass({
|
||||||
atEndOfLiveTimeline: false,
|
atEndOfLiveTimeline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this._updateTopUnreadMessagesBar();
|
||||||
},
|
},
|
||||||
|
|
||||||
onDragOver: function(ev) {
|
onDragOver: function(ev) {
|
||||||
|
@ -879,6 +882,30 @@ module.exports = React.createClass({
|
||||||
this.refs.messagePanel.jumpToLiveTimeline();
|
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
|
// get the current scroll position of the room, so that it can be
|
||||||
// restored when we switch back to it.
|
// restored when we switch back to it.
|
||||||
//
|
//
|
||||||
|
@ -1247,8 +1274,22 @@ module.exports = React.createClass({
|
||||||
eventId={this.props.eventId}
|
eventId={this.props.eventId}
|
||||||
eventPixelOffset={this.props.eventPixelOffset}
|
eventPixelOffset={this.props.eventPixelOffset}
|
||||||
onScroll={ this.onMessageListScroll }
|
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 (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||||
|
@ -1264,6 +1305,7 @@ module.exports = React.createClass({
|
||||||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||||
} />
|
} />
|
||||||
{ auxPanel }
|
{ auxPanel }
|
||||||
|
{ topUnreadMessagesBar }
|
||||||
{ messagePanel }
|
{ messagePanel }
|
||||||
{ searchResultsPanel }
|
{ searchResultsPanel }
|
||||||
<div className="mx_RoomView_statusArea">
|
<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
|
// 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
|
// 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) {
|
scrollToToken: function(scrollToken, pixelOffset) {
|
||||||
var scrollNode = this._getScrollNode();
|
var scrollNode = this._getScrollNode();
|
||||||
|
|
||||||
// default to the middle
|
// default to 1/3 of the way down
|
||||||
if (pixelOffset === undefined) {
|
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
|
// 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 INITIAL_SIZE = 20;
|
||||||
var TIMELINE_CAP = 1000; // the most events to show in a timeline
|
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;
|
var DEBUG = false;
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -43,7 +48,7 @@ if (DEBUG) {
|
||||||
*
|
*
|
||||||
* Also responsible for handling and sending read receipts.
|
* Also responsible for handling and sending read receipts.
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
var TimelinePanel = React.createClass({
|
||||||
displayName: 'TimelinePanel',
|
displayName: 'TimelinePanel',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -63,20 +68,40 @@ module.exports = React.createClass({
|
||||||
eventId: React.PropTypes.string,
|
eventId: React.PropTypes.string,
|
||||||
|
|
||||||
// where to position the event given by eventId, in pixels from the
|
// 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
|
// bottom of the viewport. If not given, will try to put the event
|
||||||
// middle of the viewprt.
|
// 1/3 of the way down the viewport.
|
||||||
eventPixelOffset: React.PropTypes.number,
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
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() {
|
getInitialState: function() {
|
||||||
|
var initialReadMarker =
|
||||||
|
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|
||||||
|
|| this._getCurrentReadReceipt();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events: [],
|
events: [],
|
||||||
timelineLoading: true, // track whether our room timeline is loading
|
timelineLoading: true, // track whether our room timeline is loading
|
||||||
canBackPaginate: true,
|
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");
|
debuglog("TimelinePanel: mounting");
|
||||||
|
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
this._resetActivityTimer();
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
|
||||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
|
@ -116,7 +142,6 @@ module.exports = React.createClass({
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
|
||||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
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) {
|
onAction: function(payload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'user_activity':
|
case 'user_activity':
|
||||||
|
this._resetActivityTimer();
|
||||||
|
|
||||||
|
// fall-through!
|
||||||
|
|
||||||
case 'user_activity_end':
|
case 'user_activity_end':
|
||||||
// we could treat user_activity_end differently and not
|
// we could treat user_activity_end differently and not
|
||||||
// send receipts for messages that have arrived between
|
// 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
|
// being active, but let's see if this is actually
|
||||||
// necessary.
|
// necessary.
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
this.updateReadMarker();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_resetActivityTimer: function() {
|
||||||
|
this.user_last_active = Date.now();
|
||||||
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||||
// ignore events for other rooms
|
// ignore events for other rooms
|
||||||
if (room !== this.props.room) return;
|
if (room !== this.props.room) return;
|
||||||
|
@ -158,6 +205,26 @@ module.exports = React.createClass({
|
||||||
// updates from pagination will happen when the paginate completes.
|
// updates from pagination will happen when the paginate completes.
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
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
|
// tell the messagepanel to go paginate itself. This in turn will cause
|
||||||
// onMessageListFillRequest to be called, which will call
|
// onMessageListFillRequest to be called, which will call
|
||||||
// _onTimelineUpdated, which will update the state with the new event -
|
// _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) {
|
onRoomRedaction: function(ev, room) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
@ -234,7 +270,9 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
var lastReadEventIndex = this._getLastDisplayedEventIndex({
|
||||||
|
ignoreOwn: true
|
||||||
|
});
|
||||||
if (lastReadEventIndex === null) return;
|
if (lastReadEventIndex === null) return;
|
||||||
|
|
||||||
var lastReadEvent = this.state.events[lastReadEventIndex];
|
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
|
/* jump down to the bottom of this room, where new events are arriving
|
||||||
*/
|
*/
|
||||||
jumpToLiveTimeline: function() {
|
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
|
/* return true if the content is fully scrolled down and we are
|
||||||
* at the end of the live timeline.
|
* at the end of the live timeline.
|
||||||
*/
|
*/
|
||||||
|
@ -289,6 +395,33 @@ module.exports = React.createClass({
|
||||||
return this.refs.messagePanel.getScrollState();
|
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) {
|
_initTimeline: function(props) {
|
||||||
var initialEvent = props.eventId;
|
var initialEvent = props.eventId;
|
||||||
var pixelOffset = props.eventPixelOffset;
|
var pixelOffset = props.eventPixelOffset;
|
||||||
|
@ -304,7 +437,7 @@ module.exports = React.createClass({
|
||||||
*
|
*
|
||||||
* @param {number?} pixelOffset offset to position the given event at
|
* @param {number?} pixelOffset offset to position the given event at
|
||||||
* (pixels from the bottom of the view). If undefined, will put the
|
* (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.
|
* returns a promise which will resolve when the load completes.
|
||||||
*/
|
*/
|
||||||
|
@ -343,6 +476,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
this.updateReadMarker();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -369,16 +503,27 @@ module.exports = React.createClass({
|
||||||
return null;
|
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;
|
var messagePanel = this.refs.messagePanel;
|
||||||
if (messagePanel === undefined) return null;
|
if (messagePanel === undefined) return null;
|
||||||
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||||
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||||
var ev = this.state.events[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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,8 +531,8 @@ module.exports = React.createClass({
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
|
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -412,6 +557,27 @@ module.exports = React.createClass({
|
||||||
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
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() {
|
render: function() {
|
||||||
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||||
|
@ -452,12 +618,15 @@ module.exports = React.createClass({
|
||||||
events={ this.state.events }
|
events={ this.state.events }
|
||||||
highlightedEventId={ this.props.highlightedEventId }
|
highlightedEventId={ this.props.highlightedEventId }
|
||||||
readMarkerEventId={ this.state.readMarkerEventId }
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
|
readMarkerVisible={ this.state.readMarkerVisible }
|
||||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={ stickyBottom }
|
||||||
onScroll={ this.props.onScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
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