mirror of https://github.com/vector-im/riot-web
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.pull/21833/head
parent
d0e3319bd9
commit
701abb6a21
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue