Merge up from develop

This commit is contained in:
wmwragg 2016-09-13 12:37:52 +01:00
commit 524eeaa315
27 changed files with 780 additions and 171 deletions

View file

@ -0,0 +1,121 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
/*
* Component which shows the filtered file using a TimelinePanel
*/
var FilePanel = React.createClass({
displayName: 'FilePanel',
propTypes: {
roomId: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
timelineSet: null,
}
},
componentWillMount: function() {
this.updateTimelineSet(this.props.roomId);
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.roomId !== this.props.roomId) {
// otherwise we race between re-rendering the TimelinePanel and setting the new timelineSet.
//
// FIXME: this race only happens because of the promise returned by getOrCreateFilter().
// We should only need to create the containsUrl filter once per login session, so in practice
// it shouldn't be being done here at all. Then we could just update the timelineSet directly
// without resetting it first, and speed up room-change.
this.setState({ timelineSet: null });
this.updateTimelineSet(nextProps.roomId);
}
},
updateTimelineSet: function(roomId) {
var client = MatrixClientPeg.get();
var room = client.getRoom(roomId);
if (room) {
var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true
},
}
}
);
// FIXME: we shouldn't be doing this every time we change room - see comment above.
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{
filter.filterId = filterId;
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
this.setState({ timelineSet: timelineSet });
},
(error)=>{
console.error("Failed to get or create file panel filter", error);
}
);
}
else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
},
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
return (
<TimelinePanel key={"filepanel_" + this.props.roomId}
className="mx_FilePanel"
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = { false }
tileShape="file_grid"
opacity={ this.props.opacity }
/>
);
}
else {
return (
<div className="mx_FilePanel">
<Loader/>
</div>
);
}
},
});
module.exports = FilePanel;

View file

@ -60,6 +60,9 @@ module.exports = React.createClass({
// true to suppress the date at the start of the timeline
suppressFirstDateSeparator: React.PropTypes.bool,
// whether to show read receipts
manageReadReceipts: React.PropTypes.bool,
// true if updates to the event list should cause the scroll panel to
// scroll down when we are at the bottom of the window. See ScrollPanel
// for more details.
@ -73,6 +76,12 @@ module.exports = React.createClass({
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// className for the panel
className: React.PropTypes.string.isRequired,
// shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string,
},
componentWillMount: function() {
@ -337,6 +346,7 @@ module.exports = React.createClass({
continuation = true;
}
/*
// Work out if this is still a continuation, as we are now showing commands
// and /me messages with their own little avatar. The case of a change of
// event type (commands) is handled above, but we need to handle the /me
@ -348,6 +358,7 @@ module.exports = React.createClass({
&& prevEvent.getContent().msgtype === 'm.emote') {
continuation = false;
}
*/
// local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators.
@ -370,7 +381,10 @@ module.exports = React.createClass({
// Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId;
var readReceipts = this._getReadReceiptsForEvent(mxEv);
var readReceipts;
if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
<li key={eventId}
@ -383,6 +397,7 @@ module.exports = React.createClass({
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
last={last} isSelectedEvent={highlight}/>
</li>
);
@ -503,7 +518,7 @@ module.exports = React.createClass({
style.opacity = this.props.opacity;
return (
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable"
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
onScroll={ this.props.onScroll }
onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest }

View file

@ -0,0 +1,65 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel
*/
var NotificationPanel = React.createClass({
displayName: 'NotificationPanel',
propTypes: {
},
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
className="mx_NotificationPanel"
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview = { false }
opacity={ this.props.opacity }
tileShape="notif"
/>
);
}
else {
console.error("No notifTimelineSet available!");
return (
<div className="mx_NotificationPanel">
<Loader/>
</div>
);
}
},
});
module.exports = NotificationPanel;

View file

@ -340,8 +340,12 @@ module.exports = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (!room) return;
if (!this.state.room || room.roomId != this.state.room.roomId) return;
// ignore events from filtered timelines
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
if (ev.getType() === "org.matrix.room.preview_urls") {
this._updatePreviewUrlVisibility(room);
}
@ -1570,7 +1574,9 @@ module.exports = React.createClass({
var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
room={this.state.room}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={true}
manageReadMarkers={true}
hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId}
eventId={this.props.eventId}
@ -1579,6 +1585,7 @@ module.exports = React.createClass({
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview }
opacity={ this.props.opacity }
className="mx_RoomView_messagePanel"
/>);
var topUnreadMessagesBar = null;

View file

@ -50,9 +50,15 @@ var TimelinePanel = React.createClass({
displayName: 'TimelinePanel',
propTypes: {
// The js-sdk Room object for the room whose timeline we are
// representing.
room: React.PropTypes.object.isRequired,
// 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,
// true to give the component a 'display: none' style.
hidden: React.PropTypes.bool,
@ -84,6 +90,12 @@ var TimelinePanel = React.createClass({
// maximum number of events to show in a timeline
timelineCap: React.PropTypes.number,
// classname to use for the messagepanel
className: React.PropTypes.string,
// shape property to be passed to EventTiles
tileShape: React.PropTypes.string,
},
statics: {
@ -97,13 +109,18 @@ var TimelinePanel = React.createClass({
getDefaultProps: function() {
return {
timelineCap: 250,
className: 'mx_RoomView_messagePanel',
};
},
getInitialState: function() {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|| this._getCurrentReadReceipt();
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
if (this.props.manageReadMarkers) {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|| this._getCurrentReadReceipt();
}
return {
events: [],
@ -137,7 +154,7 @@ var TimelinePanel = React.createClass({
canForwardPaginate: false,
// start with the read-marker visible, so that we see its animated
// disappearance when swtitching into the room.
// disappearance when switching into the room.
readMarkerVisible: true,
readMarkerEventId: initialReadMarker,
@ -163,8 +180,8 @@ var TimelinePanel = React.createClass({
},
componentWillReceiveProps: function(newProps) {
if (newProps.room !== this.props.room) {
// throw new Error("changing room on a TimelinePanel is not supported");
if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
// regrettably, this does happen; in particular, when joining a
// room with /join. In that case, there are two Rooms in
@ -175,7 +192,7 @@ var TimelinePanel = React.createClass({
//
// for now, just warn about this. But we're going to end up paginating
// both rooms separately, and it's all bad.
console.warn("Replacing room on a TimelinePanel - confusion may ensue");
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
}
if (newProps.eventId != this.props.eventId) {
@ -280,11 +297,13 @@ var TimelinePanel = React.createClass({
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});
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});
}
}
},
@ -304,8 +323,8 @@ var TimelinePanel = React.createClass({
},
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore events for other rooms
if (room !== this.props.room) return;
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
// ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
@ -337,40 +356,42 @@ var TimelinePanel = React.createClass({
var lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents());
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.room.getPendingEvents());
}
var updatedState = {events: events};
// 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
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
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
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
}
}
this.setState(updatedState, callback);
});
},
onRoomTimelineReset: function(room) {
if (room !== this.props.room) return;
onRoomTimelineReset: function(room, timelineSet) {
if (timelineSet !== this.props.timelineSet) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
this._loadTimeline();
@ -381,7 +402,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.room) return;
if (room !== this.props.timelineSet.room) return;
// we could skip an update if the event isn't in our timeline,
// but that's probably an early optimisation.
@ -392,7 +413,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.room) return;
if (room !== this.props.timelineSet.room) return;
this.forceUpdate();
},
@ -401,7 +422,7 @@ var TimelinePanel = React.createClass({
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.room) return;
if (room !== this.props.timelineSet.room) return;
this._reloadEvents();
},
@ -409,12 +430,13 @@ var TimelinePanel = React.createClass({
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
// to avoid having to wait from the remote echo from the homeserver.
if (this.isAtEndOfLiveTimeline()) {
this.props.room.setUnreadNotificationCount('total', 0);
this.props.room.setUnreadNotificationCount('highlight', 0);
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
// XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up
}
@ -461,6 +483,7 @@ var TimelinePanel = 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.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() !== 0) {
return;
}
@ -498,6 +521,8 @@ var TimelinePanel = React.createClass({
// advance the read marker past any events we sent ourselves.
_advanceReadMarkerPastMyEvents: function() {
if (!this.props.manageReadMarkers) return;
// 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.
@ -548,11 +573,9 @@ var TimelinePanel = React.createClass({
* the container.
*/
jumpToReadMarker: function() {
if (!this.refs.messagePanel)
return;
if (!this.state.readMarkerEventId)
return;
if (!this.props.manageReadMarkers) return;
if (!this.refs.messagePanel) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
// into the _timelineWindow. In that case, attempts to scroll to it
@ -579,10 +602,12 @@ var TimelinePanel = React.createClass({
/* update the read-up-to marker to match the read receipt
*/
forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return;
var rmId = this._getCurrentReadReceipt();
// see if we know the timestamp for the rr event
var tl = this.props.room.getTimelineForEvent(rmId);
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
@ -622,7 +647,9 @@ var TimelinePanel = React.createClass({
// 0: read marker is visible
// +1: read marker is below the window
getReadMarkerPosition: function() {
if (!this.refs.messagePanel) { return null; }
if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
var ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) {
return ret;
@ -630,7 +657,7 @@ var TimelinePanel = React.createClass({
// 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];
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
if (rmTs && this.state.events.length > 0) {
if (rmTs < this.state.events[0].getTs()) {
return -1;
@ -691,7 +718,7 @@ var TimelinePanel = React.createClass({
*/
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow(
MatrixClientPeg.get(), this.props.room,
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
var onLoaded = () => {
@ -745,7 +772,7 @@ var TimelinePanel = React.createClass({
// go via the dispatcher so that the URL is updated
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId,
room_id: this.props.timelineSet.roomId,
});
};
}
@ -807,7 +834,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents());
events.push(... this.props.timelineSet.getPendingEvents());
}
return events;
@ -873,11 +900,13 @@ var TimelinePanel = React.createClass({
return null;
var myUserId = client.credentials.userId;
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
},
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) {
var roomId = this.props.timelineSet.room.roomId;
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
// don't update the state (and cause a re-render) if there is
// no change to the RM.
return;
@ -885,11 +914,11 @@ var TimelinePanel = React.createClass({
// ideally we'd sync these via the server, but for now just stash them
// in a map.
TimelinePanel.roomReadMarkerMap[this.props.room.roomId] = eventId;
TimelinePanel.roomReadMarkerMap[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;
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
if (inhibitSetState) {
return;
@ -919,7 +948,7 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper">
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<Loader />
</div>
);
@ -946,11 +975,14 @@ var TimelinePanel = React.createClass({
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
opacity={ this.props.opacity }
className={ this.props.className }
tileShape={ this.props.tileShape }
/>
);
},