diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 190181c875..98b698ca3e 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -18,6 +18,8 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils';
+import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
+import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
const localStorage = window.localStorage;
@@ -104,6 +106,13 @@ class MatrixClientPeg {
this.matrixClient.setMaxListeners(500);
this.matrixClient.setGuest(Boolean(creds.guest));
+
+ var notifTimelineSet = new EventTimelineSet(null, {
+ timelineSupport: true
+ });
+ // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
+ notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
+ this.matrixClient.setNotifTimelineSet(notifTimelineSet);
}
}
diff --git a/src/Notifier.js b/src/Notifier.js
index 99fef9d671..4390083129 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -190,7 +190,7 @@ var Notifier = {
setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden;
-
+
// XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled
dis.dispatch({
@@ -224,10 +224,12 @@ var Notifier = {
}
},
- onRoomTimeline: function(ev, room, toStartOfTimeline) {
+ onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
+ if (!room) return;
if (!this.isPrepared) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
+ if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
diff --git a/src/UserActivity.js b/src/UserActivity.js
index 5c80f4743e..e7338e17e9 100644
--- a/src/UserActivity.js
+++ b/src/UserActivity.js
@@ -37,7 +37,8 @@ class UserActivity {
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
// fired when the view scrolls down for a new message.
- window.addEventListener('wheel', this._onUserActivity.bind(this), true);
+ window.addEventListener('wheel', this._onUserActivity.bind(this),
+ { passive: true, capture: true });
this.lastActivityAtTs = new Date().getTime();
this.lastDispatchAtTs = 0;
this.activityEndTimer = undefined;
@@ -50,7 +51,8 @@ class UserActivity {
document.onmousedown = undefined;
document.onmousemove = undefined;
document.onkeypress = undefined;
- window.removeEventListener('wheel', this._onUserActivity.bind(this), true);
+ window.removeEventListener('wheel', this._onUserActivity.bind(this),
+ { passive: true, capture: true });
}
/**
diff --git a/src/component-index.js b/src/component-index.js
index 4cf2ba4016..488b85670b 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -27,8 +27,10 @@ limitations under the License.
module.exports.components = {};
module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu');
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
+module.exports.components['structures.FilePanel'] = require('./components/structures/FilePanel');
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');
+module.exports.components['structures.NotificationPanel'] = require('./components/structures/NotificationPanel');
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar');
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
new file mode 100644
index 0000000000..0dd16a7e99
--- /dev/null
+++ b/src/components/structures/FilePanel.js
@@ -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 (
+
+ );
+ }
+ else {
+ return (
+
+
+
+ );
+ }
+ },
+});
+
+module.exports = FilePanel;
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 73ea2fd1a0..6f2d8f038b 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -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() {
@@ -370,7 +379,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(
);
@@ -503,7 +516,7 @@ module.exports = React.createClass({
style.opacity = this.props.opacity;
return (
-
+ );
+ }
+ else {
+ console.error("No notifTimelineSet available!");
+ return (
+
+
+
+ );
+ }
+ },
+});
+
+module.exports = NotificationPanel;
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 041493d420..49d171f631 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -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 = (
);
var topUnreadMessagesBar = null;
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index c80a8c89d7..be35e9cbdd 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -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 (
-
+
);
@@ -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 }
/>
);
},
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index dbad084024..c37cd32c4e 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -57,18 +57,34 @@ module.exports = React.createClass({
var TintableSvg = sdk.getComponent("elements.TintableSvg");
if (httpUrl) {
- return (
-
-
-
- );
+ if (this.props.tileShape === "file_grid") {
+ return (
+
+
+
+ );
+ }
+ else {
+ return (
+
+
+
+ );
+ }
} else {
- var extra = text ? ': '+text : '';
+ var extra = text ? (': ' + text) : '';
return
Invalid file{extra}
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index ec594af2ce..526fc6a3a5 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -123,6 +123,30 @@ module.exports = React.createClass({
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
+ var download;
+ if (this.props.tileShape === "file_grid") {
+ download = (
+
+
+ {content.body}
+
+
+ { content.info && content.info.size ? filesize(content.info.size) : "" }
+
+
+ );
+ }
+ else {
+ download = (
+
+ );
+ }
+
var thumbUrl = this._getThumbUrl();
if (thumbUrl) {
return (
@@ -133,12 +157,7 @@ module.exports = React.createClass({
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
-
+ { download }
);
} else if (content.body) {
diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js
index c8327a71ae..2494ab9499 100644
--- a/src/components/views/messages/MVideoBody.js
+++ b/src/components/views/messages/MVideoBody.js
@@ -69,12 +69,37 @@ module.exports = React.createClass({
}
}
+ var download;
+ if (this.props.tileShape === "file_grid") {
+ download = (
+
+
+ {content.body}
+
+
+ { content.info && content.info.size ? filesize(content.info.size) : "" }
+
+
+ );
+ }
+ else {
+ download = (
+
+ );
+ }
+
return (
+ { download }
);
},
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js
index 26658c3005..4ac0c0dabb 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.js
@@ -37,6 +37,9 @@ module.exports = React.createClass({
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
+
+ /* the shsape of the tile, used */
+ tileShape: React.PropTypes.string,
},
getEventTileOps: function() {
@@ -69,6 +72,7 @@ module.exports = React.createClass({
return
;
},
});
diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js
index 4eaa19193e..f7c3052ea8 100644
--- a/src/components/views/rooms/AuxPanel.js
+++ b/src/components/views/rooms/AuxPanel.js
@@ -93,8 +93,9 @@ module.exports = React.createClass({
}
else {
joinText = (
- Join as { this.onConferenceNotificationClick(event, 'voice')}} href="#">voice
- or { this.onConferenceNotificationClick(event, 'video') }} href="#">video.
+ Join as { this.onConferenceNotificationClick(event, 'voice')}}
+ href="#">voice or { this.onConferenceNotificationClick(event, 'video') }}
+ href="#">video.
);
}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index b1df3f3267..f3ae3dff7f 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -128,6 +128,15 @@ module.exports = React.createClass({
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
+
+ /* the shape of the tile. by default, the layout is intended for the
+ * normal room timeline. alternative values are: "file_list", "file_grid"
+ * and "notif". This could be done by CSS, but it'd be horribly inefficient.
+ * It could also be done by subclassing EventTile, but that'd be quite
+ * boiilerplatey. So just make the necessary render decisions conditional
+ * for now.
+ */
+ tileShape: React.PropTypes.string,
},
getInitialState: function() {
@@ -382,18 +391,16 @@ module.exports = React.createClass({
this.props.eventSendStatus
) !== -1,
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
- mx_EventTile_highlight: this.shouldHighlight(),
+ mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
- mx_EventTile_continuation: this.props.continuation,
+ mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
});
- var timestamp =
-
-
+ var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId();
var readAvatars = this.getReadAvatars();
@@ -401,7 +408,10 @@ module.exports = React.createClass({
let avatarSize;
let needsSenderProfile;
- if (isInfoMessage) {
+ if (this.props.tileShape === "notif") {
+ avatarSize = 24;
+ needsSenderProfile = true;
+ } else if (isInfoMessage) {
// a small avatar, with no sender profile, for emotes and
// joins/parts/etc
avatarSize = 14;
@@ -428,35 +438,93 @@ module.exports = React.createClass({
if (needsSenderProfile) {
let aux = null;
- if (msgtype === 'm.image') aux = "sent an image";
- else if (msgtype === 'm.video') aux = "sent a video";
- else if (msgtype === 'm.file') aux = "uploaded a file";
+ if (!this.props.tileShape) {
+ if (msgtype === 'm.image') aux = "sent an image";
+ else if (msgtype === 'm.video') aux = "sent a video";
+ else if (msgtype === 'm.file') aux = "uploaded a file";
+ sender =
;
+ }
+ else {
+ sender =
;
+ }
- sender =
;
}
var editButton = (
);
- return (
-
-
- { readAvatars }
+ if (this.props.tileShape === "notif") {
+ var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
+
+ return (
+
- { avatar }
- { sender }
-
- { timestamp }
-
- { editButton }
+ );
+ }
+ else if (this.props.tileShape === "file_grid") {
+ return (
+
-
- );
+ );
+ }
+ else {
+ return (
+
+
+ { readAvatars }
+
+ { avatar }
+ { sender }
+
+
+
+
+
+ { editButton }
+
+
+ );
+ }
},
});
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index fc80bf8a90..4eb7801e13 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -78,7 +78,7 @@ export default class MessageComposer extends React.Component {
let fileList = [];
for (let i=0; i
+ fileList.push(
{files[i].name}
);
}
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index bb4c6d336a..980fa622eb 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -147,8 +147,10 @@ module.exports = React.createClass({
this._updateStickyHeaders(true, scrollToPosition);
},
- onRoomTimeline: function(ev, room, toStartOfTimeline) {
+ onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
+ if (!room) return;
+ if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
},
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index bac8f5aa07..f25b5140f4 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -106,14 +106,16 @@ module.exports = React.createClass({
onMouseEnter: function() {
this.setState( { hover : true });
+ this.badgeOnMouseEnter();
},
onMouseLeave: function() {
this.setState( { hover : false });
+ this.badgeOnMouseLeave();
},
badgeOnMouseEnter: function() {
- // Only allow none guests to access the context menu
+ // Only allow non-guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
@@ -241,7 +243,7 @@ module.exports = React.createClass({
badgeContent = '\u200B';
}
- badge = { badgeContent }
;
+ badge = { badgeContent }
;
const EmojiText = sdk.getComponent('elements.EmojiText');
var label;
diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js
index 027d888d2d..993973cb1d 100644
--- a/test/components/structures/TimelinePanel-test.js
+++ b/test/components/structures/TimelinePanel-test.js
@@ -35,6 +35,7 @@ var USER_ID = '@me:localhost';
describe('TimelinePanel', function() {
var sandbox;
+ var timelineSet;
var room;
var client;
var timeline;
@@ -58,10 +59,16 @@ describe('TimelinePanel', function() {
test_utils.beforeEach(this);
sandbox = test_utils.stubClient(sandbox);
- timeline = new jssdk.EventTimeline(ROOM_ID);
room = sinon.createStubInstance(jssdk.Room);
- room.getLiveTimeline.returns(timeline);
- room.getPendingEvents.returns([]);
+ room.roomId = ROOM_ID;
+
+ timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);
+ timelineSet.getPendingEvents.returns([]);
+ timelineSet.room = room;
+
+ timeline = new jssdk.EventTimeline(timelineSet);
+
+ timelineSet.getLiveTimeline.returns(timeline);
client = peg.get();
client.credentials = {userId: USER_ID};
@@ -95,7 +102,7 @@ describe('TimelinePanel', function() {
var scrollDefer;
var panel = ReactDOM.render(
- {scrollDefer.resolve()}}
+ {scrollDefer.resolve()}}
/>,
parentDiv,
);
@@ -143,7 +150,10 @@ describe('TimelinePanel', function() {
// a new event!
var ev = mkMessage();
timeline.addEvent(ev);
- panel.onRoomTimeline(ev, room, false, false, {liveEvent: true});
+ panel.onRoomTimeline(ev, room, false, false, {
+ liveEvent: true,
+ timeline: timeline,
+ });
// that won't make much difference, because we don't paginate
// unless we're at the bottom of the timeline, but a scroll event
@@ -178,7 +188,7 @@ describe('TimelinePanel', function() {
});
var panel = ReactDOM.render(
- ,
+ ,
parentDiv
);
@@ -226,7 +236,7 @@ describe('TimelinePanel', function() {
var scrollDefer;
var panel = ReactDOM.render(
- {scrollDefer.resolve()}}
+ {scrollDefer.resolve()}}
timelineCap={TIMELINE_CAP}
/>,
parentDiv