2016-02-10 21:27:40 +03:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
2017-04-26 19:26:53 +03:00
|
|
|
Copyright 2017 Vector Creations Ltd
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
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 ReactDOM = require("react-dom");
|
|
|
|
var q = require("q");
|
|
|
|
|
|
|
|
var Matrix = require("matrix-js-sdk");
|
|
|
|
var EventTimeline = Matrix.EventTimeline;
|
|
|
|
|
|
|
|
var sdk = require('../../index');
|
|
|
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
|
|
var dis = require("../../dispatcher");
|
2016-03-08 01:27:35 +03:00
|
|
|
var ObjectUtils = require('../../ObjectUtils');
|
2016-03-21 20:29:33 +03:00
|
|
|
var Modal = require("../../Modal");
|
2016-03-22 15:15:17 +03:00
|
|
|
var UserActivity = require("../../UserActivity");
|
2016-04-05 15:14:11 +03:00
|
|
|
var KeyCode = require('../../KeyCode');
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
var PAGINATE_SIZE = 20;
|
|
|
|
var INITIAL_SIZE = 20;
|
|
|
|
|
2016-09-02 19:15:21 +03:00
|
|
|
var DEBUG = false;
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
if (DEBUG) {
|
|
|
|
// using bind means that we get to keep useful line numbers in the console
|
|
|
|
var debuglog = console.log.bind(console);
|
|
|
|
} else {
|
2017-01-20 17:22:27 +03:00
|
|
|
var debuglog = function() {};
|
2016-02-10 21:27:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Component which shows the event timeline in a room view.
|
|
|
|
*
|
|
|
|
* Also responsible for handling and sending read receipts.
|
|
|
|
*/
|
2016-02-24 20:26:34 +03:00
|
|
|
var TimelinePanel = React.createClass({
|
2016-02-10 21:27:40 +03:00
|
|
|
displayName: 'TimelinePanel',
|
|
|
|
|
|
|
|
propTypes: {
|
2016-09-06 02:59:17 +03:00
|
|
|
// The js-sdk EventTimelineSet object for the timeline sequence we are
|
|
|
|
// representing. This may or may not have a room, depending on what it's
|
|
|
|
// a timeline representing. If it has a room, we maintain RRs etc for
|
|
|
|
// that room.
|
|
|
|
timelineSet: React.PropTypes.object.isRequired,
|
|
|
|
|
|
|
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
|
|
|
manageReadReceipts: React.PropTypes.bool,
|
|
|
|
manageReadMarkers: React.PropTypes.bool,
|
2016-02-10 21:27:40 +03:00
|
|
|
|
2016-02-11 18:38:13 +03:00
|
|
|
// true to give the component a 'display: none' style.
|
2016-02-10 21:27:40 +03:00
|
|
|
hidden: React.PropTypes.bool,
|
|
|
|
|
|
|
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
|
|
|
// typically this will be either 'eventId' or undefined.
|
|
|
|
highlightedEventId: React.PropTypes.string,
|
|
|
|
|
|
|
|
// id of an event to jump to. If not given, will go to the end of the
|
|
|
|
// live timeline.
|
|
|
|
eventId: React.PropTypes.string,
|
|
|
|
|
|
|
|
// where to position the event given by eventId, in pixels from the
|
2016-02-24 20:26:34 +03:00
|
|
|
// bottom of the viewport. If not given, will try to put the event
|
2016-03-10 19:44:50 +03:00
|
|
|
// half way down the viewport.
|
2016-02-10 21:27:40 +03:00
|
|
|
eventPixelOffset: React.PropTypes.number,
|
|
|
|
|
2016-07-18 03:35:42 +03:00
|
|
|
// Should we show URL Previews
|
|
|
|
showUrlPreview: React.PropTypes.bool,
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
// callback which is called when the panel is scrolled.
|
|
|
|
onScroll: React.PropTypes.func,
|
2016-02-24 20:26:34 +03:00
|
|
|
|
|
|
|
// callback which is called when the read-up-to mark is updated.
|
|
|
|
onReadMarkerUpdated: React.PropTypes.func,
|
2016-04-12 19:18:32 +03:00
|
|
|
|
|
|
|
// opacity for dynamic UI fading effects
|
|
|
|
opacity: React.PropTypes.number,
|
2016-08-24 17:48:19 +03:00
|
|
|
|
|
|
|
// maximum number of events to show in a timeline
|
|
|
|
timelineCap: React.PropTypes.number,
|
2016-09-07 04:16:29 +03:00
|
|
|
|
|
|
|
// classname to use for the messagepanel
|
2016-09-07 23:10:31 +03:00
|
|
|
className: React.PropTypes.string,
|
2016-09-11 04:14:27 +03:00
|
|
|
|
|
|
|
// shape property to be passed to EventTiles
|
|
|
|
tileShape: React.PropTypes.string,
|
2017-02-02 19:32:10 +03:00
|
|
|
|
|
|
|
// placeholder text to use if the timeline is empty
|
|
|
|
empty: React.PropTypes.string,
|
2016-02-24 20:26:34 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
statics: {
|
|
|
|
// a map from room id to read marker event timestamp
|
|
|
|
roomReadMarkerTsMap: {},
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
2016-08-24 17:48:19 +03:00
|
|
|
getDefaultProps: function() {
|
|
|
|
return {
|
2016-11-16 17:25:52 +03:00
|
|
|
// By default, disable the timelineCap in favour of unpaginating based on
|
|
|
|
// event tile heights. (See _unpaginateEvents)
|
|
|
|
timelineCap: Number.MAX_VALUE,
|
2016-09-07 23:10:31 +03:00
|
|
|
className: 'mx_RoomView_messagePanel',
|
2016-08-24 17:48:19 +03:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
getInitialState: function() {
|
2016-09-06 02:59:17 +03:00
|
|
|
// XXX: we could track RM per TimelineSet rather than per Room.
|
|
|
|
// but for now we just do it per room for simplicity.
|
2017-04-12 17:05:39 +03:00
|
|
|
let initialReadMarker = null;
|
2016-09-06 02:59:17 +03:00
|
|
|
if (this.props.manageReadMarkers) {
|
2017-04-18 16:44:43 +03:00
|
|
|
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
2017-04-12 17:05:39 +03:00
|
|
|
if (readmarker){
|
2017-04-18 17:13:05 +03:00
|
|
|
initialReadMarker = readmarker.getContent().event_id;
|
2017-04-12 17:05:39 +03:00
|
|
|
} else {
|
|
|
|
initialReadMarker = this._getCurrentReadReceipt();
|
|
|
|
}
|
2016-09-06 02:59:17 +03:00
|
|
|
}
|
2016-02-24 20:26:34 +03:00
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
return {
|
|
|
|
events: [],
|
|
|
|
timelineLoading: true, // track whether our room timeline is loading
|
2016-04-08 16:58:24 +03:00
|
|
|
|
|
|
|
// canBackPaginate == false may mean:
|
|
|
|
//
|
|
|
|
// * we haven't (successfully) loaded the timeline yet, or:
|
|
|
|
//
|
|
|
|
// * we have got to the point where the room was created, or:
|
|
|
|
//
|
|
|
|
// * the server indicated that there were no more visible events
|
|
|
|
// (normally implying we got to the start of the room), or:
|
|
|
|
//
|
|
|
|
// * we gave up asking the server for more events
|
|
|
|
canBackPaginate: false,
|
|
|
|
|
|
|
|
// canForwardPaginate == false may mean:
|
|
|
|
//
|
|
|
|
// * we haven't (successfully) loaded the timeline yet
|
|
|
|
//
|
|
|
|
// * we have got to the end of time and are now tracking the live
|
|
|
|
// timeline, or:
|
|
|
|
//
|
|
|
|
// * the server indicated that there were no more visible events
|
|
|
|
// (not sure if this ever happens when we're not at the live
|
|
|
|
// timeline), or:
|
|
|
|
//
|
|
|
|
// * we are looking at some historical point, but gave up asking
|
|
|
|
// the server for more events
|
|
|
|
canForwardPaginate: false,
|
2016-02-24 20:26:34 +03:00
|
|
|
|
|
|
|
// start with the read-marker visible, so that we see its animated
|
2016-09-06 02:59:17 +03:00
|
|
|
// disappearance when switching into the room.
|
2016-02-24 20:26:34 +03:00
|
|
|
readMarkerVisible: true,
|
|
|
|
|
|
|
|
readMarkerEventId: initialReadMarker,
|
2016-02-24 16:38:55 +03:00
|
|
|
|
|
|
|
backPaginating: false,
|
|
|
|
forwardPaginating: false,
|
2017-04-26 19:26:53 +03:00
|
|
|
|
|
|
|
// cache of matrixClient.getSyncState() (but from the 'sync' event)
|
|
|
|
clientSyncState: null,
|
2016-02-10 21:27:40 +03:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillMount: function() {
|
|
|
|
debuglog("TimelinePanel: mounting");
|
|
|
|
|
|
|
|
this.last_rr_sent_event_id = undefined;
|
2017-04-19 12:28:38 +03:00
|
|
|
this.last_rm_sent_event_id = undefined;
|
2016-02-24 20:26:34 +03:00
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
this.dispatcherRef = dis.register(this.onAction);
|
|
|
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
2016-02-25 21:28:07 +03:00
|
|
|
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
2016-02-23 15:56:54 +03:00
|
|
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
2016-03-04 17:47:11 +03:00
|
|
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
2016-03-09 18:45:56 +03:00
|
|
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
2017-04-12 17:05:39 +03:00
|
|
|
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
2017-04-26 19:26:53 +03:00
|
|
|
MatrixClientPeg.get().on("sync", this.onSync);
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
this._initTimeline(this.props);
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillReceiveProps: function(newProps) {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (newProps.timelineSet !== this.props.timelineSet) {
|
|
|
|
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
2016-02-27 01:38:05 +03:00
|
|
|
|
|
|
|
// regrettably, this does happen; in particular, when joining a
|
|
|
|
// room with /join. In that case, there are two Rooms in
|
|
|
|
// circulation - one which is created by the MatrixClient.joinRoom
|
|
|
|
// call and used to create the RoomView, and a second which is
|
|
|
|
// created by the sync loop once the room comes back down the /sync
|
|
|
|
// pipe. Once the latter happens, our room is replaced with the new one.
|
|
|
|
//
|
|
|
|
// for now, just warn about this. But we're going to end up paginating
|
|
|
|
// both rooms separately, and it's all bad.
|
2016-09-06 02:59:17 +03:00
|
|
|
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
2016-02-10 21:27:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (newProps.eventId != this.props.eventId) {
|
|
|
|
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
|
|
|
" (was " + this.props.eventId + ")");
|
|
|
|
return this._initTimeline(newProps);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-03-08 01:27:35 +03:00
|
|
|
shouldComponentUpdate: function(nextProps, nextState) {
|
2016-04-19 20:29:25 +03:00
|
|
|
if (!ObjectUtils.shallowEqual(this.props, nextProps)) {
|
|
|
|
if (DEBUG) {
|
|
|
|
console.group("Timeline.shouldComponentUpdate: props change");
|
|
|
|
console.log("props before:", this.props);
|
|
|
|
console.log("props after:", nextProps);
|
|
|
|
console.groupEnd();
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
|
|
|
|
if (DEBUG) {
|
|
|
|
console.group("Timeline.shouldComponentUpdate: state change");
|
|
|
|
console.log("state before:", this.state);
|
|
|
|
console.log("state after:", nextState);
|
|
|
|
console.groupEnd();
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2016-03-08 01:27:35 +03:00
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
componentWillUnmount: function() {
|
|
|
|
// set a boolean to say we've been unmounted, which any pending
|
|
|
|
// promises can use to throw away their results.
|
|
|
|
//
|
|
|
|
// (We could use isMounted, but facebook have deprecated that.)
|
|
|
|
this.unmounted = true;
|
|
|
|
|
|
|
|
dis.unregister(this.dispatcherRef);
|
2016-02-16 02:04:21 +03:00
|
|
|
|
|
|
|
var client = MatrixClientPeg.get();
|
|
|
|
if (client) {
|
|
|
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
2016-02-25 21:28:07 +03:00
|
|
|
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
2016-02-23 15:56:54 +03:00
|
|
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
2016-03-04 17:47:11 +03:00
|
|
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
2016-03-09 18:45:56 +03:00
|
|
|
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
2017-04-19 12:18:25 +03:00
|
|
|
client.removeListener("Room.accountData", this.onAccountData);
|
2017-04-26 19:26:53 +03:00
|
|
|
client.removeListener("sync", this.onSync);
|
2016-02-16 02:04:21 +03:00
|
|
|
}
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
2016-11-16 17:25:52 +03:00
|
|
|
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
2017-04-05 19:52:05 +03:00
|
|
|
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
2016-11-16 17:25:52 +03:00
|
|
|
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
|
|
|
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
|
|
|
|
2017-04-04 15:28:26 +03:00
|
|
|
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
|
|
|
// this particular event should be the first or last to be unpaginated.
|
2016-11-16 17:25:52 +03:00
|
|
|
let eventId = scrollToken;
|
|
|
|
|
|
|
|
let marker = this.state.events.findIndex(
|
|
|
|
(ev) => {
|
|
|
|
return ev.getId() === eventId;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2017-04-04 13:55:53 +03:00
|
|
|
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
2016-11-16 17:25:52 +03:00
|
|
|
|
|
|
|
if (count > 0) {
|
|
|
|
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
2016-11-16 19:10:23 +03:00
|
|
|
this._timelineWindow.unpaginate(count, backwards);
|
2016-11-18 14:15:14 +03:00
|
|
|
|
|
|
|
// We can now paginate in the unpaginated direction
|
|
|
|
const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
|
2016-11-16 17:25:52 +03:00
|
|
|
this.setState({
|
2016-11-18 14:15:14 +03:00
|
|
|
[canPaginateKey]: true,
|
2016-11-16 17:25:52 +03:00
|
|
|
events: this._getEvents(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
// set off a pagination request.
|
|
|
|
onMessageListFillRequest: function(backwards) {
|
|
|
|
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
2016-04-08 16:58:24 +03:00
|
|
|
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
|
|
|
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
|
|
|
|
|
|
|
if (!this.state[canPaginateKey]) {
|
|
|
|
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
|
|
|
return q(false);
|
|
|
|
}
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
if(!this._timelineWindow.canPaginate(dir)) {
|
2016-04-08 16:58:24 +03:00
|
|
|
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
|
|
|
this.setState({[canPaginateKey]: false});
|
2016-02-10 21:27:40 +03:00
|
|
|
return q(false);
|
|
|
|
}
|
2016-04-08 16:58:24 +03:00
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
2016-04-08 16:58:24 +03:00
|
|
|
this.setState({[paginatingKey]: true});
|
2016-02-24 16:38:55 +03:00
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
|
2016-02-29 17:09:42 +03:00
|
|
|
if (this.unmounted) { return; }
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
2016-04-21 15:37:31 +03:00
|
|
|
|
|
|
|
var newState = {
|
2016-04-08 16:58:24 +03:00
|
|
|
[paginatingKey]: false,
|
|
|
|
[canPaginateKey]: r,
|
2016-04-20 01:45:51 +03:00
|
|
|
events: this._getEvents(),
|
2016-04-21 15:37:31 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// moving the window in this direction may mean that we can now
|
|
|
|
// paginate in the other where we previously could not.
|
|
|
|
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
|
|
|
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
|
|
|
if (!this.state[canPaginateOtherWayKey] &&
|
|
|
|
this._timelineWindow.canPaginate(otherDirection)) {
|
|
|
|
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
|
|
|
newState[canPaginateOtherWayKey] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(newState);
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
return r;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-01-20 17:22:27 +03:00
|
|
|
onMessageListScroll: function() {
|
2016-02-24 20:26:34 +03:00
|
|
|
if (this.props.onScroll) {
|
|
|
|
this.props.onScroll();
|
|
|
|
}
|
|
|
|
|
2016-09-06 02:59:17 +03:00
|
|
|
if (this.props.manageReadMarkers) {
|
|
|
|
// 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});
|
|
|
|
}
|
2016-02-24 20:26:34 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
onAction: function(payload) {
|
|
|
|
switch (payload.action) {
|
|
|
|
case 'user_activity':
|
|
|
|
case 'user_activity_end':
|
|
|
|
// we could treat user_activity_end differently and not
|
|
|
|
// send receipts for messages that have arrived between
|
|
|
|
// the actual user activity and the time they stopped
|
|
|
|
// being active, but let's see if this is actually
|
|
|
|
// necessary.
|
|
|
|
this.sendReadReceipt();
|
2016-02-24 20:26:34 +03:00
|
|
|
this.updateReadMarker();
|
2016-02-10 21:27:40 +03:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
2016-09-09 00:48:44 +03:00
|
|
|
// ignore events for other timeline sets
|
|
|
|
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
// ignore anything but real-time updates at the end of the room:
|
|
|
|
// updates from pagination will happen when the paginate completes.
|
|
|
|
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
if (!this.refs.messagePanel) return;
|
|
|
|
|
2016-04-19 20:29:25 +03:00
|
|
|
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
|
|
|
|
// we won't load this event now, because we don't want to push any
|
|
|
|
// events off the other end of the timeline. But we need to note
|
|
|
|
// that we can now paginate.
|
|
|
|
this.setState({canForwardPaginate: true});
|
|
|
|
return;
|
|
|
|
}
|
2016-03-01 13:41:56 +03:00
|
|
|
|
|
|
|
// tell the timeline window to try to advance itself, but not to make
|
|
|
|
// an http request to do so.
|
2016-02-10 21:27:40 +03:00
|
|
|
//
|
2016-03-01 13:41:56 +03:00
|
|
|
// we deliberately avoid going via the ScrollPanel for this call - the
|
|
|
|
// ScrollPanel might already have an active pagination promise, which
|
|
|
|
// will fail, but would stop us passing the pagination request to the
|
2016-04-19 20:29:25 +03:00
|
|
|
// timeline window.
|
2016-03-01 13:41:56 +03:00
|
|
|
//
|
|
|
|
// see https://github.com/vector-im/vector-web/issues/1035
|
2016-04-20 01:45:51 +03:00
|
|
|
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
|
|
|
if (this.unmounted) { return; }
|
|
|
|
|
|
|
|
var events = this._timelineWindow.getEvents();
|
|
|
|
var lastEv = events[events.length-1];
|
|
|
|
|
|
|
|
// if we're at the end of the live timeline, append the pending events
|
2016-09-06 02:59:17 +03:00
|
|
|
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
2017-01-20 17:22:27 +03:00
|
|
|
events.push(...this.props.timelineSet.room.getPendingEvents());
|
2016-04-20 01:45:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var updatedState = {events: events};
|
|
|
|
|
2016-09-06 02:59:17 +03:00
|
|
|
if (this.props.manageReadMarkers) {
|
|
|
|
// 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 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 the timeout on userCurrentlyActive.
|
|
|
|
//
|
|
|
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
|
|
|
var sender = ev.sender ? ev.sender.userId : null;
|
|
|
|
var callback = null;
|
|
|
|
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
|
|
|
updatedState.readMarkerVisible = true;
|
|
|
|
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
|
|
|
// we know we're stuckAtBottom, so we can advance the RM
|
|
|
|
// immediately, to save a later render cycle
|
2017-04-12 17:12:37 +03:00
|
|
|
|
2016-09-06 02:59:17 +03:00
|
|
|
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
|
|
|
updatedState.readMarkerVisible = false;
|
|
|
|
updatedState.readMarkerEventId = lastEv.getId();
|
|
|
|
callback = this.props.onReadMarkerUpdated;
|
|
|
|
}
|
2016-04-20 01:45:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(updatedState, callback);
|
|
|
|
});
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
2016-09-10 12:46:30 +03:00
|
|
|
onRoomTimelineReset: function(room, timelineSet) {
|
|
|
|
if (timelineSet !== this.props.timelineSet) return;
|
2016-02-25 21:28:07 +03:00
|
|
|
|
|
|
|
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
|
|
|
this._loadTimeline();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-03-22 18:06:52 +03:00
|
|
|
canResetTimeline: function() {
|
|
|
|
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
|
|
|
},
|
|
|
|
|
2016-02-23 15:56:54 +03:00
|
|
|
onRoomRedaction: function(ev, room) {
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
|
|
|
// ignore events for other rooms
|
2016-09-06 02:59:17 +03:00
|
|
|
if (room !== this.props.timelineSet.room) return;
|
2016-02-23 15:56:54 +03:00
|
|
|
|
|
|
|
// we could skip an update if the event isn't in our timeline,
|
|
|
|
// but that's probably an early optimisation.
|
|
|
|
this.forceUpdate();
|
|
|
|
},
|
|
|
|
|
2016-03-04 17:47:11 +03:00
|
|
|
onRoomReceipt: function(ev, room) {
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
|
|
|
// ignore events for other rooms
|
2016-09-06 02:59:17 +03:00
|
|
|
if (room !== this.props.timelineSet.room) return;
|
2016-03-04 17:47:11 +03:00
|
|
|
|
|
|
|
this.forceUpdate();
|
|
|
|
},
|
|
|
|
|
2016-03-09 18:45:56 +03:00
|
|
|
onLocalEchoUpdated: function(ev, room, oldEventId) {
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
|
|
|
// ignore events for other rooms
|
2016-09-06 02:59:17 +03:00
|
|
|
if (room !== this.props.timelineSet.room) return;
|
2016-03-09 18:45:56 +03:00
|
|
|
|
2016-03-18 01:26:06 +03:00
|
|
|
this._reloadEvents();
|
2016-03-09 18:45:56 +03:00
|
|
|
},
|
|
|
|
|
2017-04-12 17:05:39 +03:00
|
|
|
onAccountData: function(ev, room) {
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
|
|
|
// ignore events for other rooms
|
|
|
|
if (room !== this.props.timelineSet.room) return;
|
|
|
|
|
2017-04-18 16:44:43 +03:00
|
|
|
if (ev.getType() !== "m.fully_read") return;
|
2017-04-12 17:05:39 +03:00
|
|
|
|
2017-04-19 12:27:43 +03:00
|
|
|
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
|
|
|
|
// this mechanism of determining where the RM is relative to the view-port with
|
|
|
|
// one supported by the server (the client needs more than an event ID).
|
2017-04-12 17:05:39 +03:00
|
|
|
this.setState({
|
2017-04-19 12:20:53 +03:00
|
|
|
readMarkerEventId: ev.getContent().event_id,
|
2017-04-12 17:05:39 +03:00
|
|
|
}, this.props.onReadMarkerUpdated);
|
|
|
|
},
|
2016-03-09 18:45:56 +03:00
|
|
|
|
2017-04-26 19:26:53 +03:00
|
|
|
onSync: function(state, prevState, data) {
|
|
|
|
this.setState({clientSyncState: state});
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
sendReadReceipt: function() {
|
|
|
|
if (!this.refs.messagePanel) return;
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadReceipts) return;
|
2016-10-26 15:19:36 +03:00
|
|
|
// This happens on user_activity_end which is delayed, and it's
|
|
|
|
// very possible have logged out within that timeframe, so check
|
|
|
|
// we still have a client.
|
2017-05-04 18:42:41 +03:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
// if no client or client is guest don't send RR (vector-im/riot-web#3758)
|
|
|
|
if (!cli || cli.isGuest()) return;
|
2016-02-10 21:27:40 +03:00
|
|
|
|
2016-02-23 16:24:38 +03:00
|
|
|
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
2016-02-10 21:27:40 +03:00
|
|
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
|
|
|
|
|
|
|
// We want to avoid sending out read receipts when we are looking at
|
|
|
|
// events in the past which are before the latest RR.
|
|
|
|
//
|
|
|
|
// For now, let's apply a heuristic: if (a) the event corresponding to
|
|
|
|
// the latest RR (either from the server, or sent by ourselves) doesn't
|
|
|
|
// appear in our timeline, and (b) we could forward-paginate the event
|
|
|
|
// timeline, then don't send any more RRs.
|
|
|
|
//
|
|
|
|
// This isn't watertight, as we could be looking at a section of
|
|
|
|
// timeline which is *after* the latest RR (so we should actually send
|
|
|
|
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
|
|
|
// the user eventually hits the live timeline.
|
|
|
|
//
|
|
|
|
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
|
|
|
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
var lastReadEventIndex = this._getLastDisplayedEventIndex({
|
|
|
|
ignoreOwn: true
|
|
|
|
});
|
2016-02-10 21:27:40 +03:00
|
|
|
if (lastReadEventIndex === null) return;
|
|
|
|
|
|
|
|
var lastReadEvent = this.state.events[lastReadEventIndex];
|
|
|
|
|
|
|
|
// we also remember the last read receipt we sent to avoid spamming the
|
|
|
|
// same one at the server repeatedly
|
2017-04-12 17:05:39 +03:00
|
|
|
if ((lastReadEventIndex > currentReadUpToEventIndex &&
|
|
|
|
this.last_rr_sent_event_id != lastReadEvent.getId()) ||
|
|
|
|
this.last_rm_sent_event_id != this.state.readMarkerEventId) {
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
this.last_rr_sent_event_id = lastReadEvent.getId();
|
2017-04-12 17:05:39 +03:00
|
|
|
this.last_rm_sent_event_id = this.state.readMarkerEventId;
|
|
|
|
|
2017-04-19 12:14:57 +03:00
|
|
|
MatrixClientPeg.get().setRoomReadMarkers(
|
2017-04-12 17:05:39 +03:00
|
|
|
this.props.timelineSet.room.roomId,
|
|
|
|
this.state.readMarkerEventId,
|
|
|
|
lastReadEvent
|
2017-04-20 16:34:59 +03:00
|
|
|
).catch((e) => {
|
|
|
|
// /read_markers API is not implemented on this HS, fallback to just RR
|
|
|
|
if (e.errcode === 'M_UNRECOGNIZED') {
|
|
|
|
return MatrixClientPeg.get().sendReadReceipt(
|
|
|
|
lastReadEvent
|
|
|
|
).catch(() => {
|
|
|
|
this.last_rr_sent_event_id = undefined;
|
|
|
|
});
|
|
|
|
}
|
2016-02-10 21:27:40 +03:00
|
|
|
// it failed, so allow retries next time the user is active
|
|
|
|
this.last_rr_sent_event_id = undefined;
|
2017-04-12 17:05:39 +03:00
|
|
|
this.last_rm_sent_event_id = undefined;
|
2016-02-10 21:27:40 +03:00
|
|
|
});
|
2017-03-29 16:08:31 +03:00
|
|
|
|
|
|
|
// do a quick-reset of our unreadNotificationCount to avoid having
|
|
|
|
// to wait from the remote echo from the homeserver.
|
2017-03-29 16:12:50 +03:00
|
|
|
// we only do this if we're right at the end, because we're just assuming
|
|
|
|
// that sending an RR for the latest message will set our notif counter
|
|
|
|
// to zero: it may not do this if we send an RR for somewhere before the end.
|
|
|
|
if (this.isAtEndOfLiveTimeline()) {
|
|
|
|
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
|
|
|
|
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'on_room_read',
|
2017-04-18 21:28:24 +03:00
|
|
|
room: this.props.timelineSet.room,
|
2017-03-29 16:12:50 +03:00
|
|
|
});
|
|
|
|
}
|
2016-02-10 21:27:40 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
// 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() {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadMarkers) return;
|
2016-02-24 20:26:34 +03:00
|
|
|
if (this.getReadMarkerPosition() !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-03-22 19:34:12 +03:00
|
|
|
|
|
|
|
// advance the read marker past any events we sent ourselves.
|
|
|
|
_advanceReadMarkerPastMyEvents: function() {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadMarkers) return;
|
|
|
|
|
2016-03-22 19:34:12 +03:00
|
|
|
// we call _timelineWindow.getEvents() rather than using
|
|
|
|
// this.state.events, because react batches the update to the latter, so it
|
|
|
|
// may not have been updated yet.
|
|
|
|
var events = this._timelineWindow.getEvents();
|
|
|
|
|
|
|
|
// first find where the current RM is
|
|
|
|
for (var i = 0; i < events.length; i++) {
|
2017-01-20 17:22:27 +03:00
|
|
|
if (events[i].getId() == this.state.readMarkerEventId) {
|
2016-03-22 19:34:12 +03:00
|
|
|
break;
|
2017-01-20 17:22:27 +03:00
|
|
|
}
|
2016-03-22 19:34:12 +03:00
|
|
|
}
|
|
|
|
if (i >= events.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// now think about advancing it
|
|
|
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
2016-03-23 12:49:22 +03:00
|
|
|
for (i++; i < events.length; i++) {
|
2016-03-22 19:34:12 +03:00
|
|
|
var ev = events[i];
|
|
|
|
if (!ev.sender || ev.sender.userId != myUserId) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// i is now the first unread message which we didn't send ourselves.
|
|
|
|
i--;
|
|
|
|
|
|
|
|
var ev = events[i];
|
|
|
|
this._setReadMarker(ev.getId(), ev.getTs());
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
/* jump down to the bottom of this room, where new events are arriving
|
|
|
|
*/
|
|
|
|
jumpToLiveTimeline: function() {
|
|
|
|
// if we can't forward-paginate the existing timeline, then there
|
|
|
|
// is no point reloading it - just jump straight to the bottom.
|
|
|
|
//
|
|
|
|
// Otherwise, reload the timeline rather than trying to paginate
|
|
|
|
// through all of space-time.
|
|
|
|
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
|
|
|
this._loadTimeline();
|
|
|
|
} else {
|
|
|
|
if (this.refs.messagePanel) {
|
|
|
|
this.refs.messagePanel.scrollToBottom();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-03-10 19:44:50 +03:00
|
|
|
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
|
|
|
|
* the container.
|
2016-02-24 20:26:34 +03:00
|
|
|
*/
|
|
|
|
jumpToReadMarker: function() {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadMarkers) return;
|
|
|
|
if (!this.refs.messagePanel) return;
|
|
|
|
if (!this.state.readMarkerEventId) return;
|
2016-03-09 10:52:45 +03:00
|
|
|
|
|
|
|
// we may not have loaded the event corresponding to the read-marker
|
|
|
|
// into the _timelineWindow. In that case, attempts to scroll to it
|
|
|
|
// will fail.
|
|
|
|
//
|
|
|
|
// a quick way to figure out if we've loaded the relevant event is
|
|
|
|
// simply to check if the messagepanel knows where the read-marker is.
|
|
|
|
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
|
|
|
if (ret !== null) {
|
|
|
|
// The messagepanel knows where the RM is, so we must have loaded
|
|
|
|
// the relevant event.
|
2016-03-10 19:44:50 +03:00
|
|
|
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
|
|
|
|
0, 1/3);
|
2016-03-31 02:51:23 +03:00
|
|
|
return;
|
2016-03-09 10:52:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Looks like we haven't loaded the event corresponding to the read-marker.
|
|
|
|
// As with jumpToLiveTimeline, we want to reload the timeline around the
|
|
|
|
// read-marker.
|
2016-03-10 19:44:50 +03:00
|
|
|
this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
|
2016-02-24 20:26:34 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* update the read-up-to marker to match the read receipt
|
|
|
|
*/
|
|
|
|
forgetReadMarker: function() {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadMarkers) return;
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
var rmId = this._getCurrentReadReceipt();
|
|
|
|
|
|
|
|
// see if we know the timestamp for the rr event
|
2016-09-06 02:59:17 +03:00
|
|
|
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
2016-02-24 20:26:34 +03:00
|
|
|
var rmTs;
|
|
|
|
if (tl) {
|
2017-01-20 17:22:27 +03:00
|
|
|
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
2016-02-24 20:26:34 +03:00
|
|
|
if (event) {
|
|
|
|
rmTs = event.getTs();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this._setReadMarker(rmId, rmTs);
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
/* return true if the content is fully scrolled down and we are
|
|
|
|
* at the end of the live timeline.
|
|
|
|
*/
|
|
|
|
isAtEndOfLiveTimeline: function() {
|
|
|
|
return this.refs.messagePanel
|
|
|
|
&& this.refs.messagePanel.isAtBottom()
|
|
|
|
&& this._timelineWindow
|
|
|
|
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* get the current scroll state. See ScrollPanel.getScrollState for
|
|
|
|
* details.
|
|
|
|
*
|
|
|
|
* returns null if we are not mounted.
|
|
|
|
*/
|
|
|
|
getScrollState: function() {
|
|
|
|
if (!this.refs.messagePanel) { return null; }
|
|
|
|
return this.refs.messagePanel.getScrollState();
|
|
|
|
},
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
// 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() {
|
2016-09-06 02:59:17 +03:00
|
|
|
if (!this.props.manageReadMarkers) return null;
|
|
|
|
if (!this.refs.messagePanel) return null;
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
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.
|
2017-04-12 17:09:56 +03:00
|
|
|
const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
|
2016-02-27 02:05:41 +03:00
|
|
|
if (rmTs && this.state.events.length > 0) {
|
2016-02-24 20:26:34 +03:00
|
|
|
if (rmTs < this.state.events[0].getTs()) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2017-04-27 16:03:54 +03:00
|
|
|
canJumpToReadMarker: function() {
|
2017-04-27 18:52:40 +03:00
|
|
|
// 1. Do not show jump bar if neither the RM nor the RR are set.
|
|
|
|
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
|
2017-04-27 16:03:54 +03:00
|
|
|
// read messages and unread messages. We already have a badge count and the bottom
|
|
|
|
// bar to jump to "live" when we have unread messages.
|
2017-04-27 18:52:40 +03:00
|
|
|
// 3. We want to show the bar if the read-marker is off the top of the screen.
|
|
|
|
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
|
2017-04-27 16:03:54 +03:00
|
|
|
const pos = this.getReadMarkerPosition();
|
2017-04-27 18:52:40 +03:00
|
|
|
return this.state.readMarkerEventId !== null && // 1.
|
|
|
|
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
|
|
|
|
(pos < 0 || pos === null); // 3., 4.
|
2017-04-27 16:03:54 +03:00
|
|
|
},
|
|
|
|
|
2016-04-05 15:14:11 +03:00
|
|
|
/**
|
|
|
|
* called by the parent component when PageUp/Down/etc is pressed.
|
|
|
|
*
|
|
|
|
* We pass it down to the scroll panel.
|
|
|
|
*/
|
|
|
|
handleScrollKey: function(ev) {
|
|
|
|
if (!this.refs.messagePanel) { return; }
|
|
|
|
|
|
|
|
// jump to the live timeline on ctrl-end, rather than the end of the
|
|
|
|
// timeline window.
|
2017-04-23 02:49:14 +03:00
|
|
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
|
|
|
ev.keyCode == KeyCode.END)
|
|
|
|
{
|
2016-04-05 15:14:11 +03:00
|
|
|
this.jumpToLiveTimeline();
|
|
|
|
} else {
|
|
|
|
this.refs.messagePanel.handleScrollKey(ev);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
_initTimeline: function(props) {
|
|
|
|
var initialEvent = props.eventId;
|
|
|
|
var pixelOffset = props.eventPixelOffset;
|
2016-03-10 19:44:50 +03:00
|
|
|
|
|
|
|
// if a pixelOffset is given, it is relative to the bottom of the
|
|
|
|
// container. If not, put the event in the middle of the container.
|
|
|
|
var offsetBase = 1;
|
|
|
|
if (pixelOffset == null) {
|
|
|
|
offsetBase = 0.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* (re)-load the event timeline, and initialise the scroll state, centered
|
|
|
|
* around the given event.
|
|
|
|
*
|
|
|
|
* @param {string?} eventId the event to focus on. If undefined, will
|
|
|
|
* scroll to the bottom of the room.
|
|
|
|
*
|
|
|
|
* @param {number?} pixelOffset offset to position the given event at
|
2016-03-10 19:44:50 +03:00
|
|
|
* (pixels from the offsetBase). If omitted, defaults to 0.
|
|
|
|
*
|
|
|
|
* @param {number?} offsetBase the reference point for the pixelOffset. 0
|
|
|
|
* means the top of the container, 1 means the bottom, and fractional
|
|
|
|
* values mean somewhere in the middle. If omitted, it defaults to 0.
|
2016-02-10 21:27:40 +03:00
|
|
|
*
|
|
|
|
* returns a promise which will resolve when the load completes.
|
|
|
|
*/
|
2016-03-10 19:44:50 +03:00
|
|
|
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
|
2016-02-26 15:25:46 +03:00
|
|
|
this._timelineWindow = new Matrix.TimelineWindow(
|
2016-09-06 02:59:17 +03:00
|
|
|
MatrixClientPeg.get(), this.props.timelineSet,
|
2016-08-24 17:48:19 +03:00
|
|
|
{windowLimit: this.props.timelineCap});
|
2016-02-26 15:25:46 +03:00
|
|
|
|
|
|
|
var onLoaded = () => {
|
2016-03-18 01:26:06 +03:00
|
|
|
this._reloadEvents();
|
2016-02-26 15:25:46 +03:00
|
|
|
|
2016-03-22 19:34:12 +03:00
|
|
|
// If we switched away from the room while there were pending
|
|
|
|
// outgoing events, the read-marker will be before those events.
|
|
|
|
// We need to skip over any which have subsequently been sent.
|
|
|
|
this._advanceReadMarkerPastMyEvents();
|
|
|
|
|
2016-04-08 16:58:24 +03:00
|
|
|
this.setState({
|
|
|
|
canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
|
|
|
|
canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS),
|
|
|
|
timelineLoading: false,
|
|
|
|
}, () => {
|
2016-02-10 21:27:40 +03:00
|
|
|
// initialise the scroll state of the message panel
|
|
|
|
if (!this.refs.messagePanel) {
|
2016-03-18 01:26:06 +03:00
|
|
|
// this shouldn't happen - we know we're mounted because
|
|
|
|
// we're in a setState callback, and we know
|
|
|
|
// timelineLoading is now false, so render() should have
|
|
|
|
// mounted the message panel.
|
2016-02-10 21:27:40 +03:00
|
|
|
console.log("can't initialise scroll state because " +
|
|
|
|
"messagePanel didn't load");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (eventId) {
|
2016-03-10 19:44:50 +03:00
|
|
|
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
|
|
|
|
offsetBase);
|
2016-02-10 21:27:40 +03:00
|
|
|
} else {
|
|
|
|
this.refs.messagePanel.scrollToBottom();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendReadReceipt();
|
2016-02-24 20:26:34 +03:00
|
|
|
this.updateReadMarker();
|
2016-02-10 21:27:40 +03:00
|
|
|
});
|
2016-02-26 15:25:46 +03:00
|
|
|
};
|
|
|
|
|
2016-03-21 20:29:33 +03:00
|
|
|
var onError = (error) => {
|
|
|
|
this.setState({timelineLoading: false});
|
|
|
|
var msg = error.message ? error.message : JSON.stringify(error);
|
|
|
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
|
|
|
|
|
|
var onFinished;
|
|
|
|
|
|
|
|
// if we were given an event ID, then when the user closes the
|
|
|
|
// dialog, let's jump to the end of the timeline. If we weren't,
|
|
|
|
// something has gone badly wrong and rather than causing a loop of
|
|
|
|
// undismissable dialogs, let's just give up.
|
|
|
|
if (eventId) {
|
|
|
|
onFinished = () => {
|
|
|
|
// go via the dispatcher so that the URL is updated
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
2017-03-30 19:18:22 +03:00
|
|
|
room_id: this.props.timelineSet.room.roomId,
|
2016-03-21 20:29:33 +03:00
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
2016-12-22 01:25:46 +03:00
|
|
|
var message = "Tried to load a specific point in this room's timeline, but ";
|
2016-03-22 20:33:46 +03:00
|
|
|
if (error.errcode == 'M_FORBIDDEN') {
|
|
|
|
message += "you do not have permission to view the message in question.";
|
|
|
|
} else {
|
|
|
|
message += "was unable to find it.";
|
|
|
|
}
|
2016-03-21 20:29:33 +03:00
|
|
|
Modal.createDialog(ErrorDialog, {
|
2016-03-22 17:22:16 +03:00
|
|
|
title: "Failed to load timeline position",
|
2016-03-22 20:33:46 +03:00
|
|
|
description: message,
|
2016-03-21 20:29:33 +03:00
|
|
|
onFinished: onFinished,
|
|
|
|
});
|
2017-01-20 17:22:27 +03:00
|
|
|
};
|
2016-03-21 20:29:33 +03:00
|
|
|
|
|
|
|
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
|
|
|
|
|
|
|
// if we already have the event in question, TimelineWindow.load
|
|
|
|
// returns a resolved promise.
|
|
|
|
//
|
|
|
|
// In this situation, we don't really want to defer the update of the
|
|
|
|
// state to the next event loop, because it makes room-switching feel
|
|
|
|
// quite slow. So we detect that situation and shortcut straight to
|
|
|
|
// calling _reloadEvents and updating the state.
|
|
|
|
|
|
|
|
if (prom.isFulfilled()) {
|
2016-02-26 15:25:46 +03:00
|
|
|
onLoaded();
|
2016-03-21 20:29:33 +03:00
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
events: [],
|
2016-04-08 16:58:24 +03:00
|
|
|
canBackPaginate: false,
|
|
|
|
canForwardPaginate: false,
|
2016-03-21 20:29:33 +03:00
|
|
|
timelineLoading: true,
|
|
|
|
});
|
|
|
|
|
2017-01-20 17:22:27 +03:00
|
|
|
prom = prom.then(onLoaded, onError);
|
2016-02-26 15:25:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
prom.done();
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
2016-03-18 01:26:06 +03:00
|
|
|
// handle the completion of a timeline load or localEchoUpdate, by
|
|
|
|
// reloading the events from the timelinewindow and pending event list into
|
|
|
|
// the state.
|
|
|
|
_reloadEvents: function() {
|
2016-02-10 21:27:40 +03:00
|
|
|
// we might have switched rooms since the load started - just bin
|
|
|
|
// the results if so.
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
2016-04-20 01:45:51 +03:00
|
|
|
this.setState({
|
|
|
|
events: this._getEvents(),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// get the list of events from the timeline window and the pending event list
|
|
|
|
_getEvents: function() {
|
2016-03-18 01:26:06 +03:00
|
|
|
var events = this._timelineWindow.getEvents();
|
|
|
|
|
|
|
|
// if we're at the end of the live timeline, append the pending events
|
|
|
|
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
2017-01-20 17:22:27 +03:00
|
|
|
events.push(...this.props.timelineSet.getPendingEvents());
|
2016-03-18 01:26:06 +03:00
|
|
|
}
|
|
|
|
|
2016-04-20 01:45:51 +03:00
|
|
|
return events;
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_indexForEventId: function(evId) {
|
|
|
|
for (var i = 0; i < this.state.events.length; ++i) {
|
|
|
|
if (evId == this.state.events[i].getId()) {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
_getLastDisplayedEventIndex: function(opts) {
|
|
|
|
opts = opts || {};
|
|
|
|
var ignoreOwn = opts.ignoreOwn || false;
|
|
|
|
var ignoreEchoes = opts.ignoreEchoes || false;
|
|
|
|
var allowPartial = opts.allowPartial || false;
|
|
|
|
|
2016-02-10 21:27:40 +03:00
|
|
|
var messagePanel = this.refs.messagePanel;
|
|
|
|
if (messagePanel === undefined) return null;
|
|
|
|
|
|
|
|
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
2016-02-24 20:26:34 +03:00
|
|
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
for (var i = this.state.events.length-1; i >= 0; --i) {
|
|
|
|
var ev = this.state.events[i];
|
|
|
|
|
2016-02-24 20:26:34 +03:00
|
|
|
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// local echoes have a fake event ID
|
|
|
|
if (ignoreEchoes && ev.status) {
|
2016-02-10 21:27:40 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var node = messagePanel.getNodeForEventId(ev.getId());
|
|
|
|
if (!node) continue;
|
|
|
|
|
|
|
|
var boundingRect = node.getBoundingClientRect();
|
2016-02-24 20:26:34 +03:00
|
|
|
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
|
|
|
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
2016-02-10 21:27:40 +03:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* get the id of the event corresponding to our user's latest read-receipt.
|
2016-02-23 16:24:38 +03:00
|
|
|
*
|
|
|
|
* @param {Boolean} ignoreSynthesized If true, return only receipts that
|
|
|
|
* have been sent by the server, not
|
|
|
|
* implicit ones generated by the JS
|
|
|
|
* SDK.
|
2016-02-10 21:27:40 +03:00
|
|
|
*/
|
2016-02-23 16:24:38 +03:00
|
|
|
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
2016-02-10 21:27:40 +03:00
|
|
|
var client = MatrixClientPeg.get();
|
|
|
|
// the client can be null on logout
|
2017-01-20 17:22:27 +03:00
|
|
|
if (client == null) {
|
2016-02-10 21:27:40 +03:00
|
|
|
return null;
|
2017-01-20 17:22:27 +03:00
|
|
|
}
|
2016-02-10 21:27:40 +03:00
|
|
|
|
|
|
|
var myUserId = client.credentials.userId;
|
2016-09-06 02:59:17 +03:00
|
|
|
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
2016-02-10 21:27:40 +03:00
|
|
|
},
|
2016-02-11 18:38:13 +03:00
|
|
|
|
2016-04-20 01:45:51 +03:00
|
|
|
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
2016-09-06 02:59:17 +03:00
|
|
|
var roomId = this.props.timelineSet.room.roomId;
|
|
|
|
|
2017-04-19 12:46:08 +03:00
|
|
|
// don't update the state (and cause a re-render) if there is
|
|
|
|
// no change to the RM.
|
2017-04-12 17:05:39 +03:00
|
|
|
if (eventId === this.state.readMarkerEventId) {
|
2016-02-24 20:26:34 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// in order to later figure out if the read marker is
|
|
|
|
// above or below the visible timeline, we stash the timestamp.
|
2016-09-06 02:59:17 +03:00
|
|
|
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
2016-02-24 20:26:34 +03:00
|
|
|
|
2016-04-20 01:45:51 +03:00
|
|
|
if (inhibitSetState) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-12 17:05:39 +03:00
|
|
|
// Do the local echo of the RM
|
2016-02-24 20:26:34 +03:00
|
|
|
// run the render cycle before calling the callback, so that
|
|
|
|
// getReadMarkerPosition() returns the right thing.
|
|
|
|
this.setState({
|
|
|
|
readMarkerEventId: eventId,
|
|
|
|
}, this.props.onReadMarkerUpdated);
|
|
|
|
},
|
2016-02-11 18:38:13 +03:00
|
|
|
|
|
|
|
render: function() {
|
|
|
|
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
|
|
|
var Loader = sdk.getComponent("elements.Spinner");
|
|
|
|
|
|
|
|
// just show a spinner while the timeline loads.
|
|
|
|
//
|
|
|
|
// put it in a div of the right class (mx_RoomView_messagePanel) so
|
|
|
|
// that the order in the roomview flexbox is correct, and
|
|
|
|
// mx_RoomView_messageListWrapper to position the inner div in the
|
|
|
|
// right place.
|
|
|
|
//
|
|
|
|
// Note that the click-on-search-result functionality relies on the
|
|
|
|
// fact that the messagePanel is hidden while the timeline reloads,
|
|
|
|
// but that the RoomHeader (complete with search term) continues to
|
|
|
|
// exist.
|
|
|
|
if (this.state.timelineLoading) {
|
|
|
|
return (
|
2016-09-07 04:16:29 +03:00
|
|
|
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
2016-02-11 18:38:13 +03:00
|
|
|
<Loader />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-02-03 14:11:37 +03:00
|
|
|
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
2017-02-02 19:32:10 +03:00
|
|
|
return (
|
|
|
|
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
|
|
|
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-02-11 18:38:13 +03:00
|
|
|
// give the messagepanel a stickybottom if we're at the end of the
|
|
|
|
// live timeline, so that the arrival of new events triggers a
|
|
|
|
// scroll.
|
|
|
|
//
|
|
|
|
// Make sure that stickyBottom is *false* if we can paginate
|
|
|
|
// forwards, otherwise if somebody hits the bottom of the loaded
|
|
|
|
// events when viewing historical messages, we get stuck in a loop
|
|
|
|
// of paginating our way through the entire history of the room.
|
|
|
|
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
2017-04-26 16:05:09 +03:00
|
|
|
|
|
|
|
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
|
|
|
// the HS and fetch the latest events, so we are effectively forward paginating.
|
|
|
|
const forwardPaginating = (
|
2017-04-26 19:26:53 +03:00
|
|
|
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
|
2017-04-26 16:05:09 +03:00
|
|
|
);
|
|
|
|
|
2016-02-11 18:38:13 +03:00
|
|
|
return (
|
|
|
|
<MessagePanel ref="messagePanel"
|
|
|
|
hidden={ this.props.hidden }
|
2016-02-24 16:38:55 +03:00
|
|
|
backPaginating={ this.state.backPaginating }
|
2017-04-26 16:05:09 +03:00
|
|
|
forwardPaginating={ forwardPaginating }
|
2016-02-11 18:38:13 +03:00
|
|
|
events={ this.state.events }
|
|
|
|
highlightedEventId={ this.props.highlightedEventId }
|
|
|
|
readMarkerEventId={ this.state.readMarkerEventId }
|
2016-02-24 20:26:34 +03:00
|
|
|
readMarkerVisible={ this.state.readMarkerVisible }
|
2016-02-11 18:38:13 +03:00
|
|
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
2016-07-20 14:03:13 +03:00
|
|
|
showUrlPreview = { this.props.showUrlPreview }
|
2016-09-08 05:01:38 +03:00
|
|
|
manageReadReceipts = { this.props.manageReadReceipts }
|
2016-02-11 18:38:13 +03:00
|
|
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
|
|
|
stickyBottom={ stickyBottom }
|
2016-02-24 20:26:34 +03:00
|
|
|
onScroll={ this.onMessageListScroll }
|
2016-02-11 18:38:13 +03:00
|
|
|
onFillRequest={ this.onMessageListFillRequest }
|
2016-11-16 17:25:52 +03:00
|
|
|
onUnfillRequest={ this.onMessageListUnfillRequest }
|
2016-04-12 19:18:32 +03:00
|
|
|
opacity={ this.props.opacity }
|
2016-09-07 04:16:29 +03:00
|
|
|
className={ this.props.className }
|
2016-09-11 04:14:27 +03:00
|
|
|
tileShape={ this.props.tileShape }
|
2016-02-11 18:38:13 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
2016-02-10 21:27:40 +03:00
|
|
|
});
|
2016-02-24 20:26:34 +03:00
|
|
|
|
|
|
|
module.exports = TimelinePanel;
|