diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index e17ea52976..9d8ecc1da6 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -25,6 +25,7 @@ export default function DNDTagTile(props) {
key={props.tag}
draggableId={props.tag}
index={props.index}
+ type="draggable-TagTile"
>
{ (provided, snapshot) => (
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 0c0f7366eb..4a491c8a03 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');
@@ -27,14 +26,13 @@ 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';
-import Modal from '../../../Modal';
-
const HIDE_CONFERENCE_CHANS = true;
function phraseForSection(section) {
@@ -80,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);
@@ -118,6 +115,10 @@ module.exports = React.createClass({
this.updateVisibleRooms();
});
+ this._roomListStoreToken = RoomListStore.addListener(() => {
+ this._delayedRefreshRoomList();
+ });
+
this.refreshRoomList();
// order of the sublists
@@ -178,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);
@@ -251,10 +251,6 @@ module.exports = React.createClass({
this._delayedRefreshRoomList();
},
- onRoomTags: function(event, room) {
- this._delayedRefreshRoomList();
- },
-
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
},
@@ -278,106 +274,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 = 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 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;
-
- 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),
@@ -441,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.
@@ -452,70 +348,34 @@ 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;
+ const isRoomVisible = {
+ // $roomId: true,
+ };
- // 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);
- }
- } else if (me.membership === "leave") {
- lists["im.vector.fake.archived"].push(room);
- } else {
- console.error("unrecognised membership: " + me.membership + " - this should never happen");
- }
+ this._visibleRooms.forEach((r) => {
+ isRoomVisible[r.roomId] = true;
});
- // we actually apply the sorting to this when receiving the prop in RoomSubLists.
+ Object.keys(lists).forEach((tagName) => {
+ filteredLists[tagName] = lists[tagName].filter((taggedRoom) => {
+ // Somewhat impossible, but guard against it anyway
+ if (!taggedRoom) {
+ return;
+ }
+ const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
+ if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
+ return;
+ }
- // 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 Boolean(isRoomVisible[taggedRoom.roomId]);
+ });
+ });
- return lists;
+ return filteredLists;
},
_getScrollNode: function() {
@@ -752,116 +612,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 ;
+ }
+ }) }
-
+
-
-
-
-
+
+
+
);
},
});
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
new file mode 100644
index 0000000000..b71e1c5cc1
--- /dev/null
+++ b/src/stores/RoomListStore.js
@@ -0,0 +1,256 @@
+/*
+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';
+import Unread from '../Unread';
+
+/**
+ * A class for storing application state for categorising rooms in
+ * the RoomList.
+ */
+class RoomListStore extends Store {
+ constructor() {
+ super(dis);
+
+ this._init();
+ this._getManualComparator = this._getManualComparator.bind(this);
+ this._recentsComparator = this._recentsComparator.bind(this);
+ }
+
+ _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);
+ this.__emitChange();
+ }
+
+ __onDispatch(payload) {
+ switch (payload.action) {
+ // Initialise state after initial sync
+ case 'MatrixActions.sync': {
+ if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
+ break;
+ }
+
+ this._matrixClient = payload.matrixClient;
+ this._generateRoomLists();
+ }
+ break;
+ case 'MatrixActions.Room.tags': {
+ if (!this._state.ready) break;
+ this._generateRoomLists();
+ }
+ break;
+ case 'MatrixActions.accountData': {
+ if (payload.event_type !== 'm.direct') break;
+ 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': {
+ // 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;
+ case 'RoomListActions.tagRoom.failure': {
+ // 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._matrixClient = null;
+ }
+ break;
+ }
+ }
+
+ _generateRoomLists(optimisticRequest) {
+ 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();
+
+ // 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") {
+ 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
+ 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];
+ 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");
+ }
+ });
+
+ 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;
+ }
+}
+
+if (global.singletonRoomListStore === undefined) {
+ global.singletonRoomListStore = new RoomListStore();
+}
+export default global.singletonRoomListStore;