From 38de8a129bdf5359714f5721ebcfa38195cdcda4 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 25 Jan 2018 21:45:15 +0100 Subject: [PATCH 01/19] Add transaction capability to asyncActions for relating pending/success/failure actions. Particularly useful for mapping a failure to a pending action to roll back any optimistic updates. --- src/actions/actionCreators.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 0238eee8c0..697930414c 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -31,18 +31,30 @@ limitations under the License. * `${id}.pending` and either * `${id}.success` or * `${id}.failure`. + * + * The shape of each are: + * { action: '${id}.pending', request, asyncId } + * { action: '${id}.success', result, asyncId } + * { action: '${id}.failure', err, asyncId } + * + * where `request` is returned by `pendingFn`, result + * is the result of the promise returned by `fn` and + * `asyncId` is a unique ID for each dispatch of the + * asynchronous action. */ export function asyncAction(id, fn, pendingFn) { return (dispatch) => { + const asyncId = Math.random().toString(16).slice(2, 10); dispatch({ action: id + '.pending', request: typeof pendingFn === 'function' ? pendingFn() : undefined, + asyncId, }); fn().then((result) => { - dispatch({action: id + '.success', result}); + dispatch({action: id + '.success', result, asyncId}); }).catch((err) => { - dispatch({action: id + '.failure', err}); + dispatch({action: id + '.failure', err, asyncId}); }); }; } From 815f52587bd532bb916113fe205b3bbf2df6140b Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 25 Jan 2018 21:53:34 +0100 Subject: [PATCH 02/19] Move TagPanel out of LoggedInView (...and into LeftPanel in riot-web. Can we merge the projects yet?) --- src/components/structures/LoggedInView.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index e97d9dd0a1..d7fe699156 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -208,7 +208,6 @@ const LoggedInView = React.createClass({ }, render: function() { - const TagPanel = sdk.getComponent('structures.TagPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel'); const RightPanel = sdk.getComponent('structures.RightPanel'); const RoomView = sdk.getComponent('structures.RoomView'); @@ -330,7 +329,6 @@ const LoggedInView = React.createClass({
{ topBar }
- { SettingsStore.isFeatureEnabled("feature_tag_panel") ? :
} Date: Thu, 25 Jan 2018 21:58:35 +0100 Subject: [PATCH 03/19] Remove DragDropContext from TagPanel and RoomList So that we can have one context that can handle DND between the TagPanel and RoomList. --- src/components/structures/TagPanel.js | 53 ++--- src/components/views/rooms/RoomList.js | 304 ++++++++----------------- 2 files changed, 118 insertions(+), 239 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 1cd3f04f9d..8c790edc03 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -20,12 +20,11 @@ import { MatrixClient } from 'matrix-js-sdk'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; -import TagOrderActions from '../../actions/TagOrderActions'; import sdk from '../../index'; import dis from '../../dispatcher'; -import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import { Droppable } from 'react-beautiful-dnd'; const TagPanel = React.createClass({ displayName: 'TagPanel', @@ -82,22 +81,6 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'view_create_group'}); }, - onTagTileEndDrag(result) { - // Dragged to an invalid destination, not onto a droppable - if (!result.destination) { - return; - } - - // Dispatch synchronously so that the TagPanel receives an - // optimistic update from TagOrderStore before the previous - // state is shown. - dis.dispatch(TagOrderActions.moveTag( - this.context.matrixClient, - result.draggableId, - result.destination.index, - ), true); - }, - render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); @@ -112,24 +95,22 @@ const TagPanel = React.createClass({ />; }); return
- - - { (provided, snapshot) => ( -
- { tags } - { provided.placeholder } -
- ) } -
-
+ + { (provided, snapshot) => ( +
+ { tags } + { provided.placeholder } +
+ ) } +
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ca1fccd1f5..d1ef6c2f2c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -18,7 +18,6 @@ limitations under the License. 'use strict'; const React = require("react"); const ReactDOM = require("react-dom"); -import { DragDropContext } from 'react-beautiful-dnd'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const GeminiScrollbar = require('react-gemini-scrollbar'); @@ -33,8 +32,6 @@ const Receipt = require('../../../utils/Receipt'); import TagOrderStore from '../../../stores/TagOrderStore'; import GroupStoreCache from '../../../stores/GroupStoreCache'; -import Modal from '../../../Modal'; - const HIDE_CONFERENCE_CHANS = true; function phraseForSection(section) { @@ -278,103 +275,6 @@ module.exports = React.createClass({ this.forceUpdate(); }, - onRoomTileEndDrag: function(result) { - if (!result.destination) return; - - let newTag = result.destination.droppableId.split('_')[1]; - let prevTag = result.source.droppableId.split('_')[1]; - if (newTag === 'undefined') newTag = undefined; - if (prevTag === 'undefined') prevTag = undefined; - - const roomId = result.draggableId.split('_')[1]; - const room = MatrixClientPeg.get().getRoom(roomId); - - const newIndex = result.destination.index; - - // Evil hack to get DMs behaving - if ((prevTag === undefined && newTag === 'im.vector.fake.direct') || - (prevTag === 'im.vector.fake.direct' && newTag === undefined) - ) { - Rooms.guessAndSetDMRoom( - room, newTag === 'im.vector.fake.direct', - ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to set direct chat tag " + err); - Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { - title: _t('Failed to set direct chat tag'), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - return; - } - - const hasChangedSubLists = result.source.droppableId !== result.destination.droppableId; - - let newOrder = null; - - // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const newList = Object.assign({}, this.state.lists[newTag]); - - // If the room was moved "down" (increasing index) in the same list we - // need to use the orders of the tiles with indices shifted by +1 - const offset = ( - newTag === prevTag && result.source.index < result.destination.index - ) ? 1 : 0; - - const prevOrder = newIndex === 0 ? - 0 : newList[offset + newIndex - 1].tags[newTag].order; - const nextOrder = newIndex === newList.length ? - 1 : newList[offset + newIndex].tags[newTag].order; - - newOrder = { - order: (prevOrder + nextOrder) / 2.0, - }; - } - - // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with 'im.vector.fake.direct`. - // - // if we moved lists, remove the old tag - if (prevTag && prevTag !== 'im.vector.fake.direct' && - hasChangedSubLists - ) { - // Optimistic update of what will happen to the room tags - delete room.tags[prevTag]; - - MatrixClientPeg.get().deleteRoomTag(roomId, prevTag).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to remove tag " + prevTag + " from room: " + err); - Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { - title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - } - - // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== 'im.vector.fake.direct' && - (hasChangedSubLists || newOrder) - ) { - // Optimistic update of what will happen to the room tags - room.tags[newTag] = newOrder; - - MatrixClientPeg.get().setRoomTag(roomId, newTag, newOrder).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to add tag " + newTag + " to room: " + err); - Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { - title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - } - - // Refresh to display the optimistic updates - this needs to be done in the - // same tick as the drag finishing otherwise the room will pop back to its - // previous position - hence no delayed refresh - this.refreshRoomList(); - }, - _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -749,116 +649,114 @@ module.exports = React.createClass({ const self = this; return ( - - -
- + +
+ - + - + - + - + - { Object.keys(self.state.lists).map((tagName) => { - if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - return ; - } - }) } + { Object.keys(self.state.lists).map((tagName) => { + if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + return ; + } + }) } - + - -
-
- + +
+
); }, }); From 701abb6a219cb278dc992c4761ef58d366e29743 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 25 Jan 2018 22:16:03 +0100 Subject: [PATCH 04/19] Move management of room lists to RoomListStore this is part maintenance to make RoomList clearer and part allowing room list state to be modified via a dispatch. --- src/actions/MatrixActionCreators.js | 5 + src/actions/RoomListActions.js | 142 +++++++++++++++++ src/components/views/rooms/RoomList.js | 91 +++-------- src/stores/RoomListStore.js | 212 +++++++++++++++++++++++++ 4 files changed, 383 insertions(+), 67 deletions(-) create mode 100644 src/actions/RoomListActions.js create mode 100644 src/stores/RoomListStore.js diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 33bdb53799..d9309d7c1c 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -62,6 +62,10 @@ function createAccountDataAction(matrixClient, accountDataEvent) { }; } +function createRoomTagsAction(matrixClient, roomTagsEvent, room) { + return { action: 'MatrixActions.Room.tags', room }; +} + /** * This object is responsible for dispatching actions when certain events are emitted by * the given MatrixClient. @@ -78,6 +82,7 @@ export default { start(matrixClient) { this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); }, /** diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js new file mode 100644 index 0000000000..3e0ea53a33 --- /dev/null +++ b/src/actions/RoomListActions.js @@ -0,0 +1,142 @@ +/* +Copyright 2018 New Vector 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. +*/ + +import { asyncAction } from './actionCreators'; +import RoomListStore from '../stores/RoomListStore'; + +import Modal from '../Modal'; +import Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import sdk from '../index'; + +const RoomListActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {function} an action thunk. + * @see asyncAction + */ +RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { + let metaData = null; + + // Is the tag ordered manually? + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const lists = RoomListStore.getRoomLists(); + const newList = [...lists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const prevOrder = newIndex === 0 ? + 0 : newList[offset + newIndex - 1].tags[newTag].order; + const nextOrder = newIndex === newList.length ? + 1 : newList[offset + newIndex].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === 'im.vector.fake.direct') || + (oldTag === 'im.vector.fake.direct' && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === 'im.vector.fake.direct', + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with 'im.vector.fake.direct`. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== 'im.vector.fake.direct' && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== 'im.vector.fake.direct' && + (hasChangedSubLists || metaData) + ) { + // Optimistic update of what will happen to the room tags + room.tags[newTag] = metaData; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); +}; + +export default RoomListActions; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index d1ef6c2f2c..ad85beac12 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -26,10 +26,11 @@ const CallHandler = require('../../../CallHandler'); const dis = require("../../../dispatcher"); const sdk = require('../../../index'); const rate_limited_func = require('../../../ratelimitedfunc'); -const Rooms = require('../../../Rooms'); +import * as Rooms from '../../../Rooms'; import DMRoomMap from '../../../utils/DMRoomMap'; const Receipt = require('../../../utils/Receipt'); import TagOrderStore from '../../../stores/TagOrderStore'; +import RoomListStore from '../../../stores/RoomListStore'; import GroupStoreCache from '../../../stores/GroupStoreCache'; const HIDE_CONFERENCE_CHANS = true; @@ -77,7 +78,6 @@ module.exports = React.createClass({ cli.on("deleteRoom", this.onDeleteRoom); cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); - cli.on("Room.tags", this.onRoomTags); cli.on("Room.receipt", this.onRoomReceipt); cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); @@ -115,6 +115,10 @@ module.exports = React.createClass({ this.updateVisibleRooms(); }); + this._roomListStoreToken = RoomListStore.addListener(() => { + this._delayedRefreshRoomList(); + }); + this.refreshRoomList(); // order of the sublists @@ -175,7 +179,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); - MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); @@ -248,10 +251,6 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); }, - onRoomTags: function(event, room) { - this._delayedRefreshRoomList(); - }, - onRoomStateEvents: function(ev, state) { this._delayedRefreshRoomList(); }, @@ -338,7 +337,7 @@ module.exports = React.createClass({ totalRooms += l.length; } this.setState({ - lists: this.getRoomLists(), + lists, totalRoomCount: totalRooms, // Do this here so as to not render every time the selected tags // themselves change. @@ -349,70 +348,28 @@ module.exports = React.createClass({ }, getRoomLists: function() { - const lists = {}; - lists["im.vector.fake.invite"] = []; - lists["m.favourite"] = []; - lists["im.vector.fake.recent"] = []; - lists["im.vector.fake.direct"] = []; - lists["m.lowpriority"] = []; - lists["im.vector.fake.archived"] = []; + const lists = RoomListStore.getRoomLists(); - const dmRoomMap = DMRoomMap.shared(); + const filteredLists = {}; - this._visibleRooms.forEach((room, index) => { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - if (!me) return; - - // console.log("room = " + room.name + ", me.membership = " + me.membership + - // ", sender = " + me.events.member.getSender() + - // ", target = " + me.events.member.getStateKey() + - // ", prevMembership = " + me.events.member.getPrevContent().membership); - - if (me.membership == "invite") { - lists["im.vector.fake.invite"].push(room); - } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) { - // skip past this room & don't put it in any lists - } else if (me.membership == "join" || me.membership === "ban" || - (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { - // Used to split rooms via tags - const tagNames = Object.keys(room.tags); - if (tagNames.length) { - for (let i = 0; i < tagNames.length; i++) { - const tagName = tagNames[i]; - lists[tagName] = lists[tagName] || []; - lists[tagName].push(room); - } - } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { - // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) - lists["im.vector.fake.direct"].push(room); - } else { - lists["im.vector.fake.recent"].push(room); + Object.keys(lists).forEach((tagName) => { + filteredLists[tagName] = lists[tagName].filter((taggedRoom) => { + // Somewhat impossible, but guard against it anyway + if (!taggedRoom) { + return; } - } else if (me.membership === "leave") { - lists["im.vector.fake.archived"].push(room); - } else { - console.error("unrecognised membership: " + me.membership + " - this should never happen"); - } + const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId); + if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) { + return; + } + + return this._visibleRooms.some((visibleRoom) => { + return visibleRoom.roomId === taggedRoom.roomId; + }); + }); }); - // we actually apply the sorting to this when receiving the prop in RoomSubLists. - - // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down -/* - this.listOrder = [ - "im.vector.fake.invite", - "m.favourite", - "im.vector.fake.recent", - "im.vector.fake.direct", - Object.keys(otherTagNames).filter(tagName=>{ - return (!tagName.match(/^m\.(favourite|lowpriority)$/)); - }).sort(), - "m.lowpriority", - "im.vector.fake.archived" - ]; -*/ - - return lists; + return filteredLists; }, _getScrollNode: function() { diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js new file mode 100644 index 0000000000..16902ef471 --- /dev/null +++ b/src/stores/RoomListStore.js @@ -0,0 +1,212 @@ +/* +Copyright 2018 New Vector 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. +*/ +import {Store} from 'flux/utils'; +import dis from '../dispatcher'; +import DMRoomMap from '../utils/DMRoomMap'; + +/** + * A class for storing application state for categorising rooms in + * the RoomList. + */ +class RoomListStore extends Store { + constructor() { + super(dis); + + this._init(); + this._actionHistory = []; + } + + _init() { + // Initialise state + this._state = { + lists: { + "im.vector.fake.invite": [], + "m.favourite": [], + "im.vector.fake.recent": [], + "im.vector.fake.direct": [], + "m.lowpriority": [], + "im.vector.fake.archived": [], + }, + ready: false, + }; + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + console.info(this._state); + this.__emitChange(); + } + + __onDispatch(payload) { + switch (payload.action) { + // Initialise state after initial sync + case 'MatrixActions.sync': { + if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) { + break; + } + + this._generateRoomLists(payload.matrixClient); + this._actionHistory.unshift(payload); + } + break; + case 'MatrixActions.Room.tags': { + if (!this._state.ready) break; + this._updateRoomLists(payload.room); + this._actionHistory.unshift(payload); + } + break; + case 'RoomListActions.tagRoom.pending': { + this._updateRoomListsOptimistic( + payload.request.room, + payload.request.oldTag, + payload.request.newTag, + payload.request.metaData, + ); + this._actionHistory.unshift(payload); + } + break; + case 'RoomListActions.tagRoom.failure': { + this._actionHistory = this._actionHistory.filter((action) => { + return action.asyncId !== payload.asyncId; + }); + + // don't duplicate history + const history = this._actionHistory.slice(0); + this._actionHistory = []; + this._reloadFromHistory(history); + } + break; + case 'on_logged_out': { + // Reset state without pushing an update to the view, which generally assumes that + // the matrix client isn't `null` and so causing a re-render will cause NPEs. + this._init(); + this._actionHistory.unshift(payload); + } + break; + } + } + + _reloadFromHistory(history) { + this._init(); + history.forEach((action) => this.__onDispatch(action)); + } + + _updateRoomListsOptimistic(updatedRoom, oldTag, newTag, metaData) { + const newLists = {}; + + // Remove room from oldTag + Object.keys(this._state.lists).forEach((tagName) => { + if (tagName === oldTag) { + newLists[tagName] = this._state.lists[tagName].filter((room) => { + return room.roomId !== updatedRoom.roomId; + }); + } else { + newLists[tagName] = this._state.lists[tagName]; + } + }); + + /// XXX: RoomSubList sorts by data on the room object. We + /// should sort in advance and incrementally insert new rooms + /// instead of resorting every time. + if (metaData) { + updatedRoom.tags[newTag] = metaData; + } + + newLists[newTag].push(updatedRoom); + + this._setState({ + lists: newLists, + }); + } + + _updateRoomLists(updatedRoom) { + const roomTags = Object.keys(updatedRoom.tags); + + const newLists = {}; + + // Removal of the updatedRoom from tags it no longer has + Object.keys(this._state.lists).forEach((tagName) => { + newLists[tagName] = this._state.lists[tagName].filter((room) => { + return room.roomId !== updatedRoom.roomId || roomTags.includes(tagName); + }); + }); + + roomTags.forEach((tagName) => { + if (newLists[tagName].includes(updatedRoom)) return; + newLists[tagName].push(updatedRoom); + }); + + this._setState({ + lists: newLists, + }); + } + + _generateRoomLists(matrixClient) { + const lists = { + "im.vector.fake.invite": [], + "m.favourite": [], + "im.vector.fake.recent": [], + "im.vector.fake.direct": [], + "m.lowpriority": [], + "im.vector.fake.archived": [], + }; + + const dmRoomMap = DMRoomMap.shared(); + + matrixClient.getRooms().forEach((room, index) => { + const me = room.getMember(matrixClient.credentials.userId); + if (!me) return; + + if (me.membership == "invite") { + lists["im.vector.fake.invite"].push(room); + } else if (me.membership == "join" || me.membership === "ban" || + (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { + // Used to split rooms via tags + const tagNames = Object.keys(room.tags); + if (tagNames.length) { + for (let i = 0; i < tagNames.length; i++) { + const tagName = tagNames[i]; + lists[tagName] = lists[tagName] || []; + lists[tagName].push(room); + } + } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { + // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) + lists["im.vector.fake.direct"].push(room); + } else { + lists["im.vector.fake.recent"].push(room); + } + } else if (me.membership === "leave") { + lists["im.vector.fake.archived"].push(room); + } else { + console.error("unrecognised membership: " + me.membership + " - this should never happen"); + } + }); + + this._setState({ + lists, + ready: true, // Ready to receive updates via Room.tags events + }); + } + + getRoomLists() { + return this._state.lists; + } +} + +if (global.singletonRoomListStore === undefined) { + global.singletonRoomListStore = new RoomListStore(); +} +export default global.singletonRoomListStore; From 73e3a594ac20dece69ff3f9b3aa644153703dca4 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 25 Jan 2018 22:52:19 +0100 Subject: [PATCH 05/19] Prevent TagTiles from being dragged into other droppables for the time being at least. --- src/components/structures/TagPanel.js | 5 ++++- src/components/views/elements/DNDTagTile.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 8c790edc03..c843c08ce9 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -95,7 +95,10 @@ const TagPanel = React.createClass({ />; }); return
- + { (provided, snapshot) => (
{ (provided, snapshot) => (
From 4820a195ab1d75f318030c4313309ab319d23a29 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Fri, 26 Jan 2018 09:15:03 +0100 Subject: [PATCH 06/19] Remove logging --- src/stores/RoomListStore.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 16902ef471..69d957f074 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -46,7 +46,6 @@ class RoomListStore extends Store { _setState(newState) { this._state = Object.assign(this._state, newState); - console.info(this._state); this.__emitChange(); } From 330ce0f02e31a7be7195624718fb3caf5ad1d11a Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 5 Feb 2018 17:29:22 +0000 Subject: [PATCH 07/19] On failure, regenerate state from sdk Instead of using history, which could be unpredictable --- src/stores/RoomListStore.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 69d957f074..e48eacaa6a 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -26,7 +26,6 @@ class RoomListStore extends Store { super(dis); this._init(); - this._actionHistory = []; } _init() { @@ -57,14 +56,13 @@ class RoomListStore extends Store { break; } - this._generateRoomLists(payload.matrixClient); - this._actionHistory.unshift(payload); + this._matrixClient = payload.matrixClient; + this._generateRoomLists(); } break; case 'MatrixActions.Room.tags': { if (!this._state.ready) break; this._updateRoomLists(payload.room); - this._actionHistory.unshift(payload); } break; case 'RoomListActions.tagRoom.pending': { @@ -74,35 +72,22 @@ class RoomListStore extends Store { payload.request.newTag, payload.request.metaData, ); - this._actionHistory.unshift(payload); } break; case 'RoomListActions.tagRoom.failure': { - this._actionHistory = this._actionHistory.filter((action) => { - return action.asyncId !== payload.asyncId; - }); - - // don't duplicate history - const history = this._actionHistory.slice(0); - this._actionHistory = []; - this._reloadFromHistory(history); + // Reset state according to js-sdk + this._generateRoomLists(); } break; case 'on_logged_out': { // Reset state without pushing an update to the view, which generally assumes that // the matrix client isn't `null` and so causing a re-render will cause NPEs. this._init(); - this._actionHistory.unshift(payload); } break; } } - _reloadFromHistory(history) { - this._init(); - history.forEach((action) => this.__onDispatch(action)); - } - _updateRoomListsOptimistic(updatedRoom, oldTag, newTag, metaData) { const newLists = {}; @@ -153,7 +138,7 @@ class RoomListStore extends Store { }); } - _generateRoomLists(matrixClient) { + _generateRoomLists() { const lists = { "im.vector.fake.invite": [], "m.favourite": [], @@ -163,10 +148,14 @@ class RoomListStore extends Store { "im.vector.fake.archived": [], }; + const dmRoomMap = DMRoomMap.shared(); - matrixClient.getRooms().forEach((room, index) => { - const me = room.getMember(matrixClient.credentials.userId); + // If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync + if (!this._matrixClient) return; + + this._matrixClient.getRooms().forEach((room, index) => { + const me = room.getMember(this._matrixClient.credentials.userId); if (!me) return; if (me.membership == "invite") { From 9982efbd8f61b75c4d4c688ce0124b47fb6c9deb Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 5 Feb 2018 18:06:29 +0000 Subject: [PATCH 08/19] Regenerate room lists when we get m.direct --- src/stores/RoomListStore.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index e48eacaa6a..6a9217eab4 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -65,6 +65,11 @@ class RoomListStore extends Store { this._updateRoomLists(payload.room); } break; + case 'MatrixActions.accountData': { + if (payload.event_type !== 'm.direct') break; + this._generateRoomLists(); + } + break; case 'RoomListActions.tagRoom.pending': { this._updateRoomListsOptimistic( payload.request.room, From c665c1170b419ec8b6e6b4b5fe7e7e06a792d0bd Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 5 Feb 2018 18:27:50 +0000 Subject: [PATCH 09/19] Regenerate room lists when we get RoomMember.membership --- src/actions/MatrixActionCreators.js | 5 +++++ src/stores/RoomListStore.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index d9309d7c1c..f4d6c34ff5 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -66,6 +66,10 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) { return { action: 'MatrixActions.Room.tags', room }; } +function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) { + return { action: 'MatrixActions.RoomMember.membership', member }; +} + /** * This object is responsible for dispatching actions when certain events are emitted by * the given MatrixClient. @@ -83,6 +87,7 @@ export default { this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); + this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction); }, /** diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 6a9217eab4..28ffee99d2 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -70,6 +70,11 @@ class RoomListStore extends Store { this._generateRoomLists(); } break; + case 'MatrixActions.RoomMember.membership': { + if (!this._matrixClient || payload.member.userId !== this._matrixClient.credentials.userId) break; + this._generateRoomLists(); + } + break; case 'RoomListActions.tagRoom.pending': { this._updateRoomListsOptimistic( payload.request.room, From feca1707f119bab11c836a810ec9566df7de6498 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 09:55:58 +0000 Subject: [PATCH 10/19] Remove a factor n complexity during room visibility calc --- src/components/views/rooms/RoomList.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ad85beac12..269f04c963 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -352,6 +352,14 @@ module.exports = React.createClass({ const filteredLists = {}; + const isRoomVisible = { + // $roomId: true, + }; + + this._visibleRooms.forEach((r) => { + isRoomVisible[r.roomId] = true; + }); + Object.keys(lists).forEach((tagName) => { filteredLists[tagName] = lists[tagName].filter((taggedRoom) => { // Somewhat impossible, but guard against it anyway @@ -363,9 +371,7 @@ module.exports = React.createClass({ return; } - return this._visibleRooms.some((visibleRoom) => { - return visibleRoom.roomId === taggedRoom.roomId; - }); + return isRoomVisible[taggedRoom.roomId]; }); }); From b744dbaab7d7feb3008f144a369d7f7cb4309eea Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 11:56:55 +0000 Subject: [PATCH 11/19] Handle setting a newTag without metaData metaData is actually the request body for the PUT that adds the tag so we need to send {} for e.g. m.lowpriority, which is not manually ordered. --- src/actions/RoomListActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index 3e0ea53a33..5802dbcaed 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -114,7 +114,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, (hasChangedSubLists || metaData) ) { // Optimistic update of what will happen to the room tags - room.tags[newTag] = metaData; + room.tags[newTag] = metaData || {}; const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From 8d0d0b43ffa6440bf98577c2246d6ec78fa3ab76 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 12:00:06 +0000 Subject: [PATCH 12/19] Handle first tag added/last tag removed This is a special case because untagged rooms should appear in im.vector.fake.recent and tagged rooms should not. --- src/stores/RoomListStore.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 28ffee99d2..32aab47009 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -101,6 +101,16 @@ class RoomListStore extends Store { _updateRoomListsOptimistic(updatedRoom, oldTag, newTag, metaData) { const newLists = {}; + // Adding a tag to an untagged room - need to remove it from recents + if (newTag && Object.keys(updatedRoom.tags).length === 0) { + oldTag = 'im.vector.fake.recent'; + } + + // Removing a tag from a room with one tag left - need to add it to recents + if (oldTag && Object.keys(updatedRoom.tags).length === 1) { + newTag = 'im.vector.fake.recent'; + } + // Remove room from oldTag Object.keys(this._state.lists).forEach((tagName) => { if (tagName === oldTag) { From dd0e981d728e9cb9974ed49d0cc46b48f1dea7ae Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 12:00:23 +0000 Subject: [PATCH 13/19] Handle indication from server that a room has no more tags --- src/stores/RoomListStore.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 32aab47009..42c77d5237 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -153,6 +153,10 @@ class RoomListStore extends Store { newLists[tagName].push(updatedRoom); }); + if (roomTags.length === 0) { + newLists['im.vector.fake.recent'].unshift(updatedRoom); + } + this._setState({ lists: newLists, }); From 1ea6301ecaf7154acc4b862f2d7f7639ea32e711 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 14:25:33 +0000 Subject: [PATCH 14/19] Add index fix again This was changed on /develop to fix an issue where the incorrect index was being used in a condition to handle literal edge cases of dragging room tiles to start or end of an ordered sublist. --- src/actions/RoomListActions.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index 5802dbcaed..f59e9953ee 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -56,10 +56,13 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newTag === oldTag && oldIndex < newIndex ) ? 1 : 0; - const prevOrder = newIndex === 0 ? - 0 : newList[offset + newIndex - 1].tags[newTag].order; - const nextOrder = newIndex === newList.length ? - 1 : newList[offset + newIndex].tags[newTag].order; + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; metaData = { order: (prevOrder + nextOrder) / 2.0, From 21d70125e4e21d40320c793186527d96c9b9358c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 8 Feb 2018 14:48:29 +0000 Subject: [PATCH 15/19] Dispatch MatrixActions synchronously Otherwise we risk blocking the dispatches on other work, and they do not need to be done asynchronously. This emerged as a bug where the room list appeared empty until MatrixActions.sync dispatches all occured in one big lump, well after the sync events being emitted by the js-sdk. --- src/actions/MatrixActionCreators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index f4d6c34ff5..dbfe910533 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -101,7 +101,7 @@ export default { */ _addMatrixClientListener(matrixClient, eventName, actionCreator) { const listener = (...args) => { - dis.dispatch(actionCreator(matrixClient, ...args)); + dis.dispatch(actionCreator(matrixClient, ...args), true); }; matrixClient.on(eventName, listener); this._matrixClientListenersStop.push(() => { From 9b0df1914953223cbbf31b33231585d1ba6f5afb Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 6 Feb 2018 15:15:47 +0000 Subject: [PATCH 16/19] Make RoomListStore aware of sub list orderings so that it can do optimistic updates of ordered lists. --- src/actions/RoomListActions.js | 5 +- src/stores/RoomListStore.js | 173 +++++++++++++++++++-------------- 2 files changed, 103 insertions(+), 75 deletions(-) diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index f59e9953ee..a92bd1ebaf 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -116,8 +116,9 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, if (newTag && newTag !== 'im.vector.fake.direct' && (hasChangedSubLists || metaData) ) { - // Optimistic update of what will happen to the room tags - room.tags[newTag] = metaData || {}; + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 42c77d5237..193784811c 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -16,6 +16,7 @@ limitations under the License. import {Store} from 'flux/utils'; import dis from '../dispatcher'; import DMRoomMap from '../utils/DMRoomMap'; +import Unread from '../Unread'; /** * A class for storing application state for categorising rooms in @@ -26,6 +27,8 @@ class RoomListStore extends Store { super(dis); this._init(); + this._getManualComparator = this._getManualComparator.bind(this); + this._recentsComparator = this._recentsComparator.bind(this); } _init() { @@ -62,7 +65,7 @@ class RoomListStore extends Store { break; case 'MatrixActions.Room.tags': { if (!this._state.ready) break; - this._updateRoomLists(payload.room); + this._generateRoomLists(); } break; case 'MatrixActions.accountData': { @@ -76,12 +79,7 @@ class RoomListStore extends Store { } break; case 'RoomListActions.tagRoom.pending': { - this._updateRoomListsOptimistic( - payload.request.room, - payload.request.oldTag, - payload.request.newTag, - payload.request.metaData, - ); + this._generateRoomLists(payload.request); } break; case 'RoomListActions.tagRoom.failure': { @@ -93,76 +91,13 @@ class RoomListStore extends Store { // Reset state without pushing an update to the view, which generally assumes that // the matrix client isn't `null` and so causing a re-render will cause NPEs. this._init(); + this._matrixClient = null; } break; } } - _updateRoomListsOptimistic(updatedRoom, oldTag, newTag, metaData) { - const newLists = {}; - - // Adding a tag to an untagged room - need to remove it from recents - if (newTag && Object.keys(updatedRoom.tags).length === 0) { - oldTag = 'im.vector.fake.recent'; - } - - // Removing a tag from a room with one tag left - need to add it to recents - if (oldTag && Object.keys(updatedRoom.tags).length === 1) { - newTag = 'im.vector.fake.recent'; - } - - // Remove room from oldTag - Object.keys(this._state.lists).forEach((tagName) => { - if (tagName === oldTag) { - newLists[tagName] = this._state.lists[tagName].filter((room) => { - return room.roomId !== updatedRoom.roomId; - }); - } else { - newLists[tagName] = this._state.lists[tagName]; - } - }); - - /// XXX: RoomSubList sorts by data on the room object. We - /// should sort in advance and incrementally insert new rooms - /// instead of resorting every time. - if (metaData) { - updatedRoom.tags[newTag] = metaData; - } - - newLists[newTag].push(updatedRoom); - - this._setState({ - lists: newLists, - }); - } - - _updateRoomLists(updatedRoom) { - const roomTags = Object.keys(updatedRoom.tags); - - const newLists = {}; - - // Removal of the updatedRoom from tags it no longer has - Object.keys(this._state.lists).forEach((tagName) => { - newLists[tagName] = this._state.lists[tagName].filter((room) => { - return room.roomId !== updatedRoom.roomId || roomTags.includes(tagName); - }); - }); - - roomTags.forEach((tagName) => { - if (newLists[tagName].includes(updatedRoom)) return; - newLists[tagName].push(updatedRoom); - }); - - if (roomTags.length === 0) { - newLists['im.vector.fake.recent'].unshift(updatedRoom); - } - - this._setState({ - lists: newLists, - }); - } - - _generateRoomLists() { + _generateRoomLists(optimisticRequest) { const lists = { "im.vector.fake.invite": [], "m.favourite": [], @@ -187,7 +122,19 @@ class RoomListStore extends Store { } else if (me.membership == "join" || me.membership === "ban" || (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { // Used to split rooms via tags - const tagNames = Object.keys(room.tags); + let tagNames = Object.keys(room.tags); + + if (optimisticRequest && optimisticRequest.room === room) { + // Remove old tag + tagNames = tagNames.filter((tagName) => tagName !== optimisticRequest.oldTag); + // Add new tag + if (optimisticRequest.newTag && + !tagNames.includes(optimisticRequest.newTag) + ) { + tagNames.push(optimisticRequest.newTag); + } + } + if (tagNames.length) { for (let i = 0; i < tagNames.length; i++) { const tagName = tagNames[i]; @@ -207,12 +154,92 @@ class RoomListStore extends Store { } }); + const listOrders = { + "manual": [ + "m.favourite", + ], + "recent": [ + "im.vector.fake.invite", + "im.vector.fake.recent", + "im.vector.fake.direct", + "m.lowpriority", + "im.vector.fake.archived", + ], + }; + + Object.keys(listOrders).forEach((order) => { + listOrders[order].forEach((listKey) => { + let comparator; + switch (order) { + case "manual": + comparator = this._getManualComparator(listKey, optimisticRequest); + break; + case "recent": + comparator = this._recentsComparator; + break; + } + lists[listKey].sort(comparator); + }); + }); + this._setState({ lists, ready: true, // Ready to receive updates via Room.tags events }); } + _tsOfNewestEvent(room) { + for (let i = room.timeline.length - 1; i >= 0; --i) { + const ev = room.timeline[i]; + if (ev.getTs() && + (Unread.eventTriggersUnreadCount(ev) || + (ev.getSender() === this._matrixClient.credentials.userId)) + ) { + return ev.getTs(); + } + } + + // we might only have events that don't trigger the unread indicator, + // in which case use the oldest event even if normally it wouldn't count. + // This is better than just assuming the last event was forever ago. + if (room.timeline.length && room.timeline[0].getTs()) { + return room.timeline[0].getTs(); + } else { + return Number.MAX_SAFE_INTEGER; + } + } + + _recentsComparator(roomA, roomB) { + return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA); + } + + _lexicographicalComparator(roomA, roomB) { + return roomA.name > roomB.name ? 1 : -1; + } + + _getManualComparator(tagName, optimisticRequest) { + return (roomA, roomB) => { + let metaA = roomA.tags[tagName]; + let metaB = roomB.tags[tagName]; + + if (optimisticRequest && roomA === optimisticRequest.room) metaA = optimisticRequest.metaData; + if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData; + + // Make sure the room tag has an order element, if not set it to be the bottom + const a = metaA.order; + const b = metaB.order; + + // Order undefined room tag orders to the bottom + if (a === undefined && b !== undefined) { + return 1; + } else if (a !== undefined && b === undefined) { + return -1; + } + + return a == b ? this._lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1); + }; + } + getRoomLists() { return this._state.lists; } From 7a4c1994c327e4616a801c0a5d4aa1495fbdbb67 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 12 Feb 2018 18:35:13 +0000 Subject: [PATCH 17/19] Use Boolean() instead of assuming filter is based on truthiness --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 269f04c963..4a491c8a03 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -371,7 +371,7 @@ module.exports = React.createClass({ return; } - return isRoomVisible[taggedRoom.roomId]; + return Boolean(isRoomVisible[taggedRoom.roomId]); }); }); From 3eeef064bf5e7abaca367f2c62dce174d9707644 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 12 Feb 2018 18:37:54 +0000 Subject: [PATCH 18/19] Remove unused asyncId --- src/actions/actionCreators.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 697930414c..967ce609e7 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -33,28 +33,25 @@ limitations under the License. * `${id}.failure`. * * The shape of each are: - * { action: '${id}.pending', request, asyncId } - * { action: '${id}.success', result, asyncId } - * { action: '${id}.failure', err, asyncId } + * { action: '${id}.pending', request } + * { action: '${id}.success', result } + * { action: '${id}.failure', err } * - * where `request` is returned by `pendingFn`, result - * is the result of the promise returned by `fn` and - * `asyncId` is a unique ID for each dispatch of the - * asynchronous action. + * where `request` is returned by `pendingFn` and + * result is the result of the promise returned by + * `fn`. */ export function asyncAction(id, fn, pendingFn) { return (dispatch) => { - const asyncId = Math.random().toString(16).slice(2, 10); dispatch({ action: id + '.pending', request: typeof pendingFn === 'function' ? pendingFn() : undefined, - asyncId, }); fn().then((result) => { - dispatch({action: id + '.success', result, asyncId}); + dispatch({action: id + '.success', result}); }).catch((err) => { - dispatch({action: id + '.failure', err, asyncId}); + dispatch({action: id + '.failure', err}); }); }; } From 322012cf889c00fe5b2bc790a01c52e6ca1e689e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 12 Feb 2018 18:46:36 +0000 Subject: [PATCH 19/19] Add comment to explain hacky optimism --- src/stores/RoomListStore.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 193784811c..b71e1c5cc1 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -79,6 +79,11 @@ class RoomListStore extends Store { } break; case 'RoomListActions.tagRoom.pending': { + // XXX: we only show one optimistic update at any one time. + // Ideally we should be making a list of in-flight requests + // that are backed by transaction IDs. Until the js-sdk + // supports this, we're stuck with only being able to use + // the most recent optimistic update. this._generateRoomLists(payload.request); } break;