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/Rooms.js b/src/Rooms.js index 436580371e..281ff842e9 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -93,14 +93,12 @@ export function setDMRoom(roomId, userId) { if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent(); + // remove it from the lists of any others users + // (it can only be a DM room for one person) for (const thisUserId of Object.keys(dmRoomMap)) { const roomList = dmRoomMap[thisUserId]; - if (thisUserId == userId) { - if (roomList.indexOf(roomId) == -1) { - roomList.push(roomId); - } - } else { + if (thisUserId != userId) { const indexOfRoom = roomList.indexOf(roomId); if (indexOfRoom > -1) { roomList.splice(indexOfRoom, 1); @@ -108,6 +106,14 @@ export function setDMRoom(roomId, userId) { } } + // now add it, if it's not already there + const roomList = dmRoomMap[userId] || []; + if (roomList.indexOf(roomId) == -1) { + roomList.push(roomId); + } + dmRoomMap[userId] = roomList; + + return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); } 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 c35b287257..11c711d239 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'); @@ -47,6 +49,7 @@ module.exports.components['views.create_room.Presets'] = require('./components/v module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias'); module.exports.components['views.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog'); module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog'); +module.exports.components['views.dialogs.EncryptedEventDialog'] = require('./components/views/dialogs/EncryptedEventDialog'); module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.MultiInviteDialog'] = require('./components/views/dialogs/MultiInviteDialog'); 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..31ef15b6dc 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() { @@ -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(
  • ); @@ -503,7 +518,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 = (