From ae34f2ed5c567680d2b9a675475e4e8b160649c9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 30 Aug 2016 01:11:26 +0100 Subject: [PATCH] placeholder NotificationPanel --- src/component-index.js | 1 + .../structures/NotificationPanel.js | 702 +----------------- 2 files changed, 4 insertions(+), 699 deletions(-) diff --git a/src/component-index.js b/src/component-index.js index 3871f60e15..6dbda67943 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -29,6 +29,7 @@ module.exports.components['structures.ContextualMenu'] = require('./components/s module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); 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/NotificationPanel.js b/src/components/structures/NotificationPanel.js index e16a95ecc4..12bf9750a7 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -16,716 +16,20 @@ limitations under the License. var React = require('react'); var ReactDOM = require("react-dom"); -var q = require("q"); - -var Matrix = require("matrix-js-sdk"); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var dis = require("../../dispatcher"); -var ObjectUtils = require('../../ObjectUtils'); -var Modal = require("../../Modal"); -var UserActivity = require("../../UserActivity"); -var KeyCode = require('../../KeyCode'); - -var PAGINATE_SIZE = 20; -var INITIAL_SIZE = 20; -var TIMELINE_CAP = 250; // the most events to show in a timeline - -var DEBUG = false; - -if (DEBUG) { - // using bind means that we get to keep useful line numbers in the console - var debuglog = console.log.bind(console); -} else { - var debuglog = function () {}; -} /* - * Component which shows the notification timeline, based on TimelinePanel + * Component which shows the notification timeline using a TimelinePanel */ var NotificationPanel = React.createClass({ displayName: 'NotificationPanel', - propTypes: { - // where to position the event given by eventId, in pixels from the - // bottom of the viewport. If not given, will try to put the event - // half way down the viewport. - eventPixelOffset: React.PropTypes.number, - - // callback which is called when the panel is scrolled. - onScroll: React.PropTypes.func, - - // opacity for dynamic UI fading effects - opacity: React.PropTypes.number, - }, - - getInitialState: function() { - return { - events: [], - notificationsLoading: true, // track whether our room timeline is loading - - // canBackPaginate == false may mean: - // - // * we haven't (successfully) loaded the notifications yet, or: - // - // * the server indicated that there were no more visible events - // (normally implying we got to the start of time), or: - // - // * we gave up asking the server for more events - canBackPaginate: false, - - // canForwardPaginate == false may mean: - // - // * we haven't (successfully) loaded the notifications yet - // - // * we have got to the end of time and are now tracking live - // notifications - // - // * we are looking at some historical point, but gave up asking - // the server for more events - canForwardPaginate: false, - - backPaginating: false, - forwardPaginating: false, - }; - }, - - componentWillMount: function() { - debuglog("NotificationPanel: mounting"); - - MatrixClientPeg.get().on("Notification.timeline", this.onNotificationTimeline); - - this._initNotification(this.props); - }, - - shouldComponentUpdate: function(nextProps, nextState) { - if (!ObjectUtils.shallowEqual(this.props, nextProps)) { - if (DEBUG) { - console.group("NotificationPanel.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("NotificationPanel.shouldComponentUpdate: state change"); - console.log("state before:", this.state); - console.log("state after:", nextState); - console.groupEnd(); - } - return true; - } - - return false; - }, - - 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; - - var client = MatrixClientPeg.get(); - if (client) { - client.removeListener("Notification.timeline", this.onNotificationTimeline); - } - }, - - // set off a pagination request. - onMessageListFillRequest: function(backwards) { - var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; - var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate'; - var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating'; - - if (!this.state[canPaginateKey]) { - debuglog("NotificationPanel: have given up", dir, "paginating notifications"); - return q(false); - } - - if(!this._timelineWindow.canPaginate(dir)) { - debuglog("TimelinePanel: can't", dir, "paginate any further"); - this.setState({[canPaginateKey]: false}); - return q(false); - } - - debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); - this.setState({[paginatingKey]: true}); - - return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => { - if (this.unmounted) { return; } - - debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); - - var newState = { - [paginatingKey]: false, - [canPaginateKey]: r, - events: this._getEvents(), - }; - - // 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); - - return r; - }); - }, - - onMessageListScroll: function () { - if (this.props.onScroll) { - this.props.onScroll(); - } - - // we hide the read marker when it first comes onto the screen, but if - // it goes back off the top of the screen (presumably because the user - // clicks on the 'jump to bottom' button), we need to re-enable it. - if (this.getReadMarkerPosition() < 0) { - this.setState({readMarkerVisible: true}); - } - }, - - onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { - // ignore events for other rooms - if (room !== this.props.room) return; - - // 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; - - if (!this.refs.messagePanel) return; - - 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; - } - - // tell the timeline window to try to advance itself, but not to make - // an http request to do so. - // - // 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 - // timeline window. - // - // see https://github.com/vector-im/vector-web/issues/1035 - 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 - if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(... this.props.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; - } - - this.setState(updatedState, callback); - }); - }, - - onRoomTimelineReset: function(room) { - if (room !== this.props.room) return; - - if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) { - this._loadTimeline(); - } - }, - - onRoomRedaction: function(ev, room) { - if (this.unmounted) return; - - // ignore events for other rooms - if (room !== this.props.room) return; - - // we could skip an update if the event isn't in our timeline, - // but that's probably an early optimisation. - this.forceUpdate(); - }, - - onRoomReceipt: function(ev, room) { - if (this.unmounted) return; - - // ignore events for other rooms - if (room !== this.props.room) return; - - this.forceUpdate(); - }, - - onLocalEchoUpdated: function(ev, room, oldEventId) { - if (this.unmounted) return; - - // ignore events for other rooms - if (room !== this.props.room) return; - - this._reloadEvents(); - }, - - /* 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(); - } - } - }, - - /* scroll to show the read-up-to marker. We put it 1/3 of the way down - * the container. - */ - jumpToReadMarker: function() { - 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 - // 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. - this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId, - 0, 1/3); - return; - } - - // 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. - this._loadTimeline(this.state.readMarkerEventId, 0, 1/3); - }, - - - /* update the read-up-to marker to match the read receipt - */ - forgetReadMarker: function() { - var rmId = this._getCurrentReadReceipt(); - - // see if we know the timestamp for the rr event - var tl = this.props.room.getTimelineForEvent(rmId); - var rmTs; - if (tl) { - var event = tl.getEvents().find((e) => { return e.getId() == rmId }); - if (event) { - rmTs = event.getTs(); - } - } - - this._setReadMarker(rmId, rmTs); - }, - - /* return true if the content is fully scrolled down and we are - * 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(); - }, - - // returns one of: - // - // null: there is no read marker - // -1: read marker is above the window - // 0: read marker is visible - // +1: read marker is below the window - getReadMarkerPosition: function() { - if (!this.refs.messagePanel) { return null; } - var ret = this.refs.messagePanel.getReadMarkerPosition(); - if (ret !== null) { - return ret; - } - - // the messagePanel doesn't know where the read marker is. - // if we know the timestamp of the read marker, make a guess based on that. - var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId]; - if (rmTs && this.state.events.length > 0) { - if (rmTs < this.state.events[0].getTs()) { - return -1; - } else { - return 1; - } - } - - return null; - }, - - /** - * 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. - if (ev.ctrlKey && ev.keyCode == KeyCode.END) { - this.jumpToLiveTimeline(); - } else { - this.refs.messagePanel.handleScrollKey(ev); - } - }, - - _initTimeline: function(props) { - var initialEvent = props.eventId; - var pixelOffset = props.eventPixelOffset; - - // 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); - }, - - /** - * (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 - * (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. - * - * returns a promise which will resolve when the load completes. - */ - _loadTimeline: function(eventId, pixelOffset, offsetBase) { - this._timelineWindow = new Matrix.TimelineWindow( - MatrixClientPeg.get(), this.props.room, - {windowLimit: TIMELINE_CAP}); - - var onLoaded = () => { - this._reloadEvents(); - - // 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(); - - this.setState({ - canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS), - canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS), - timelineLoading: false, - }, () => { - // initialise the scroll state of the message panel - if (!this.refs.messagePanel) { - // 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. - console.log("can't initialise scroll state because " + - "messagePanel didn't load"); - return; - } - if (eventId) { - this.refs.messagePanel.scrollToEvent(eventId, pixelOffset, - offsetBase); - } else { - this.refs.messagePanel.scrollToBottom(); - } - - this.sendReadReceipt(); - this.updateReadMarker(); - }); - }; - - 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', - room_id: this.props.room.roomId, - }); - }; - } - var message = "Vector was trying to load a specific point in this room's timeline but "; - if (error.errcode == 'M_FORBIDDEN') { - message += "you do not have permission to view the message in question."; - } else { - message += "was unable to find it."; - } - Modal.createDialog(ErrorDialog, { - title: "Failed to load timeline position", - description: message, - onFinished: onFinished, - }); - } - - 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()) { - onLoaded(); - } else { - this.setState({ - events: [], - canBackPaginate: false, - canForwardPaginate: false, - timelineLoading: true, - }); - - prom = prom.then(onLoaded, onError) - } - - prom.done(); - }, - - // 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() { - // we might have switched rooms since the load started - just bin - // the results if so. - if (this.unmounted) return; - - this.setState({ - events: this._getEvents(), - }); - }, - - // get the list of events from the timeline window and the pending event list - _getEvents: function() { - 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)) { - events.push(... this.props.room.getPendingEvents()); - } - - return events; - }, - - _indexForEventId: function(evId) { - for (var i = 0; i < this.state.events.length; ++i) { - if (evId == this.state.events[i].getId()) { - return i; - } - } - return null; - }, - - _getLastDisplayedEventIndex: function(opts) { - opts = opts || {}; - var ignoreOwn = opts.ignoreOwn || false; - var ignoreEchoes = opts.ignoreEchoes || false; - var allowPartial = opts.allowPartial || false; - - var messagePanel = this.refs.messagePanel; - if (messagePanel === undefined) return null; - - var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect(); - var myUserId = MatrixClientPeg.get().credentials.userId; - - for (var i = this.state.events.length-1; i >= 0; --i) { - var ev = this.state.events[i]; - - if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) { - continue; - } - - // local echoes have a fake event ID - if (ignoreEchoes && ev.status) { - continue; - } - - var node = messagePanel.getNodeForEventId(ev.getId()); - if (!node) continue; - - var boundingRect = node.getBoundingClientRect(); - if ((allowPartial && boundingRect.top < wrapperRect.bottom) || - (!allowPartial && boundingRect.bottom < wrapperRect.bottom)) { - return i; - } - } - return null; - }, - - /** - * get the id of the event corresponding to our user's latest read-receipt. - * - * @param {Boolean} ignoreSynthesized If true, return only receipts that - * have been sent by the server, not - * implicit ones generated by the JS - * SDK. - */ - _getCurrentReadReceipt: function(ignoreSynthesized) { - var client = MatrixClientPeg.get(); - // the client can be null on logout - if (client == null) - return null; - - var myUserId = client.credentials.userId; - return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized); - }, - - _setReadMarker: function(eventId, eventTs, inhibitSetState) { - if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) { - // don't update the state (and cause a re-render) if there is - // no change to the RM. - return; - } - - // ideally we'd sync these via the server, but for now just stash them - // in a map. - TimelinePanel.roomReadMarkerMap[this.props.room.roomId] = eventId; - - // in order to later figure out if the read marker is - // above or below the visible timeline, we stash the timestamp. - TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs; - - if (inhibitSetState) { - return; - } - - // run the render cycle before calling the callback, so that - // getReadMarkerPosition() returns the right thing. - this.setState({ - readMarkerEventId: eventId, - }, this.props.onReadMarkerUpdated); - }, - 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 ( -
- -
- ); - } - - // 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); - - return ( -