2018-01-25 21:16:03 +00:00
|
|
|
/*
|
|
|
|
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';
|
2018-02-06 15:15:47 +00:00
|
|
|
import Unread from '../Unread';
|
2018-10-12 20:35:54 +00:00
|
|
|
import SettingsStore from "../settings/SettingsStore";
|
2018-01-25 21:16:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A class for storing application state for categorising rooms in
|
|
|
|
* the RoomList.
|
|
|
|
*/
|
|
|
|
class RoomListStore extends Store {
|
2018-02-16 14:16:50 +00:00
|
|
|
static _listOrders = {
|
|
|
|
"m.favourite": "manual",
|
|
|
|
"im.vector.fake.invite": "recent",
|
|
|
|
"im.vector.fake.recent": "recent",
|
|
|
|
"im.vector.fake.direct": "recent",
|
|
|
|
"m.lowpriority": "recent",
|
|
|
|
"im.vector.fake.archived": "recent",
|
|
|
|
};
|
|
|
|
|
2018-01-25 21:16:03 +00:00
|
|
|
constructor() {
|
|
|
|
super(dis);
|
|
|
|
|
|
|
|
this._init();
|
2018-02-06 15:15:47 +00:00
|
|
|
this._getManualComparator = this._getManualComparator.bind(this);
|
|
|
|
this._recentsComparator = this._recentsComparator.bind(this);
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_init() {
|
|
|
|
// Initialise state
|
|
|
|
this._state = {
|
|
|
|
lists: {
|
2018-08-06 17:00:40 +00:00
|
|
|
"m.server_notice": [],
|
2018-01-25 21:16:03 +00:00
|
|
|
"im.vector.fake.invite": [],
|
|
|
|
"m.favourite": [],
|
|
|
|
"im.vector.fake.recent": [],
|
|
|
|
"im.vector.fake.direct": [],
|
|
|
|
"m.lowpriority": [],
|
|
|
|
"im.vector.fake.archived": [],
|
|
|
|
},
|
|
|
|
ready: false,
|
2018-11-01 23:17:01 +00:00
|
|
|
|
|
|
|
// The room cache stores a mapping of roomId to cache record.
|
|
|
|
// Each cache record is a key/value pair for various bits of
|
|
|
|
// data used to sort the room list. Currently this stores the
|
|
|
|
// following bits of informations:
|
|
|
|
// "timestamp": number, The timestamp of the last relevant
|
|
|
|
// event in the room.
|
|
|
|
// "notifications": boolean, Whether or not the user has been
|
|
|
|
// highlighted on any unread events.
|
|
|
|
// "unread": boolean, Whether or not the user has any
|
|
|
|
// unread events.
|
|
|
|
//
|
|
|
|
// All of the cached values are lazily loaded on read in the
|
|
|
|
// recents comparator. When an event is received for a particular
|
|
|
|
// room, all the cached values are invalidated - forcing the
|
|
|
|
// next read to set new values. The entries do not expire on
|
|
|
|
// their own.
|
|
|
|
roomCache: {},
|
2018-01-25 21:16:03 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_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;
|
|
|
|
}
|
|
|
|
|
2018-02-05 17:29:22 +00:00
|
|
|
this._matrixClient = payload.matrixClient;
|
|
|
|
this._generateRoomLists();
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'MatrixActions.Room.tags': {
|
|
|
|
if (!this._state.ready) break;
|
2018-02-06 15:15:47 +00:00
|
|
|
this._generateRoomLists();
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
break;
|
2018-02-16 14:16:50 +00:00
|
|
|
case 'MatrixActions.Room.timeline': {
|
|
|
|
if (!this._state.ready ||
|
|
|
|
!payload.isLiveEvent ||
|
2018-02-19 09:56:03 +00:00
|
|
|
!payload.isLiveUnfilteredRoomTimelineEvent ||
|
2018-02-16 14:16:50 +00:00
|
|
|
!this._eventTriggersRecentReorder(payload.event)
|
|
|
|
) break;
|
2018-11-01 22:28:13 +00:00
|
|
|
|
|
|
|
this._clearCachedRoomState(payload.event.getRoomId());
|
2018-02-16 14:16:50 +00:00
|
|
|
this._generateRoomLists();
|
|
|
|
}
|
|
|
|
break;
|
2018-02-23 10:32:33 +00:00
|
|
|
// When an event is decrypted, it could mean we need to reorder the room
|
|
|
|
// list because we now know the type of the event.
|
|
|
|
case 'MatrixActions.Event.decrypted': {
|
2018-02-23 14:54:00 +00:00
|
|
|
// We may not have synced or done an initial generation of the lists
|
|
|
|
if (!this._matrixClient || !this._state.ready) break;
|
|
|
|
|
|
|
|
const roomId = payload.event.getRoomId();
|
|
|
|
|
|
|
|
// We may have decrypted an event without a roomId (e.g to_device)
|
|
|
|
if (!roomId) break;
|
|
|
|
|
|
|
|
const room = this._matrixClient.getRoom(roomId);
|
|
|
|
|
|
|
|
// We somehow decrypted an event for a room our client is unaware of
|
|
|
|
if (!room) break;
|
|
|
|
|
2018-02-23 10:32:33 +00:00
|
|
|
const liveTimeline = room.getLiveTimeline();
|
|
|
|
const eventTimeline = room.getTimelineForEvent(payload.event.getId());
|
|
|
|
|
2018-02-23 14:54:00 +00:00
|
|
|
// Either this event was not added to the live timeline (e.g. pagination)
|
|
|
|
// or it doesn't affect the ordering of the room list.
|
|
|
|
if (liveTimeline !== eventTimeline ||
|
2018-02-23 10:32:33 +00:00
|
|
|
!this._eventTriggersRecentReorder(payload.event)
|
|
|
|
) break;
|
2018-11-01 22:28:13 +00:00
|
|
|
|
|
|
|
this._clearCachedRoomState(payload.event.getRoomId());
|
2018-02-23 10:32:33 +00:00
|
|
|
this._generateRoomLists();
|
|
|
|
}
|
|
|
|
break;
|
2018-02-05 18:06:29 +00:00
|
|
|
case 'MatrixActions.accountData': {
|
|
|
|
if (payload.event_type !== 'm.direct') break;
|
|
|
|
this._generateRoomLists();
|
|
|
|
}
|
|
|
|
break;
|
2018-09-17 17:14:52 +00:00
|
|
|
case 'MatrixActions.Room.myMembership': {
|
2018-02-05 18:27:50 +00:00
|
|
|
this._generateRoomLists();
|
|
|
|
}
|
|
|
|
break;
|
2018-02-20 17:57:46 +00:00
|
|
|
// This could be a new room that we've been invited to, joined or created
|
|
|
|
// we won't get a RoomMember.membership for these cases if we're not already
|
|
|
|
// a member.
|
|
|
|
case 'MatrixActions.Room': {
|
|
|
|
if (!this._state.ready || !this._matrixClient.credentials.userId) break;
|
|
|
|
this._generateRoomLists();
|
|
|
|
}
|
|
|
|
break;
|
2018-01-25 21:16:03 +00:00
|
|
|
case 'RoomListActions.tagRoom.pending': {
|
2018-02-12 18:46:36 +00:00
|
|
|
// 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.
|
2018-02-06 15:15:47 +00:00
|
|
|
this._generateRoomLists(payload.request);
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'RoomListActions.tagRoom.failure': {
|
2018-02-05 17:29:22 +00:00
|
|
|
// Reset state according to js-sdk
|
|
|
|
this._generateRoomLists();
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
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();
|
2018-02-06 15:15:47 +00:00
|
|
|
this._matrixClient = null;
|
2018-01-25 21:16:03 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-06 15:15:47 +00:00
|
|
|
_generateRoomLists(optimisticRequest) {
|
2018-01-25 21:16:03 +00:00
|
|
|
const lists = {
|
2018-08-06 17:00:40 +00:00
|
|
|
"m.server_notice": [],
|
2018-01-25 21:16:03 +00:00
|
|
|
"im.vector.fake.invite": [],
|
|
|
|
"m.favourite": [],
|
|
|
|
"im.vector.fake.recent": [],
|
|
|
|
"im.vector.fake.direct": [],
|
|
|
|
"m.lowpriority": [],
|
|
|
|
"im.vector.fake.archived": [],
|
|
|
|
};
|
|
|
|
|
2018-02-05 17:29:22 +00:00
|
|
|
|
2018-01-25 21:16:03 +00:00
|
|
|
const dmRoomMap = DMRoomMap.shared();
|
|
|
|
|
2018-02-05 17:29:22 +00:00
|
|
|
// If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync
|
|
|
|
if (!this._matrixClient) return;
|
|
|
|
|
|
|
|
this._matrixClient.getRooms().forEach((room, index) => {
|
2018-07-25 14:08:44 +00:00
|
|
|
const myUserId = this._matrixClient.getUserId();
|
2018-08-14 09:47:05 +00:00
|
|
|
const membership = room.getMyMembership();
|
2018-07-25 14:08:44 +00:00
|
|
|
const me = room.getMember(myUserId);
|
2018-01-25 21:16:03 +00:00
|
|
|
|
2018-07-25 12:14:36 +00:00
|
|
|
if (membership == "invite") {
|
2018-01-25 21:16:03 +00:00
|
|
|
lists["im.vector.fake.invite"].push(room);
|
2018-07-25 14:08:44 +00:00
|
|
|
} else if (membership == "join" || membership === "ban" || (me && me.isKicked())) {
|
2018-01-25 21:16:03 +00:00
|
|
|
// Used to split rooms via tags
|
2018-02-06 15:15:47 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 15:58:54 +00:00
|
|
|
// ignore any m. tag names we don't know about
|
|
|
|
tagNames = tagNames.filter((t) => {
|
|
|
|
return !t.startsWith('m.') || lists[t] !== undefined;
|
|
|
|
});
|
|
|
|
|
2018-01-25 21:16:03 +00:00
|
|
|
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);
|
|
|
|
}
|
2018-07-25 12:14:36 +00:00
|
|
|
} else if (membership === "leave") {
|
2018-01-25 21:16:03 +00:00
|
|
|
lists["im.vector.fake.archived"].push(room);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-11-01 20:43:15 +00:00
|
|
|
// Note: we check the settings up here instead of in the forEach or
|
|
|
|
// in the _recentsComparator to avoid hitting the SettingsStore a few
|
|
|
|
// thousand times.
|
|
|
|
const pinUnread = SettingsStore.getValue("pinUnreadRooms");
|
|
|
|
const pinMentioned = SettingsStore.getValue("pinMentionedRooms");
|
2018-02-14 11:23:29 +00:00
|
|
|
Object.keys(lists).forEach((listKey) => {
|
|
|
|
let comparator;
|
2018-02-16 14:16:50 +00:00
|
|
|
switch (RoomListStore._listOrders[listKey]) {
|
2018-02-14 11:23:29 +00:00
|
|
|
case "recent":
|
2018-11-01 20:46:39 +00:00
|
|
|
comparator = (roomA, roomB) => {
|
2018-11-01 22:30:48 +00:00
|
|
|
return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned);
|
2018-11-01 20:46:39 +00:00
|
|
|
};
|
2018-02-14 11:23:29 +00:00
|
|
|
break;
|
|
|
|
case "manual":
|
|
|
|
default:
|
|
|
|
comparator = this._getManualComparator(listKey, optimisticRequest);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
lists[listKey].sort(comparator);
|
2018-02-06 15:15:47 +00:00
|
|
|
});
|
|
|
|
|
2018-01-25 21:16:03 +00:00
|
|
|
this._setState({
|
|
|
|
lists,
|
|
|
|
ready: true, // Ready to receive updates via Room.tags events
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-01 22:28:13 +00:00
|
|
|
_updateCachedRoomState(roomId, type, value) {
|
|
|
|
const roomCache = this._state.roomCache;
|
|
|
|
if (!roomCache[roomId]) roomCache[roomId] = {};
|
|
|
|
|
|
|
|
if (value) roomCache[roomId][type] = value;
|
|
|
|
else delete roomCache[roomId][type];
|
|
|
|
|
|
|
|
this._setState({roomCache});
|
|
|
|
}
|
|
|
|
|
|
|
|
_clearCachedRoomState(roomId) {
|
|
|
|
const roomCache = this._state.roomCache;
|
|
|
|
delete roomCache[roomId];
|
|
|
|
this._setState({roomCache});
|
|
|
|
}
|
|
|
|
|
|
|
|
_getRoomState(room, type) {
|
|
|
|
const roomId = room.roomId;
|
|
|
|
const roomCache = this._state.roomCache;
|
|
|
|
if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') {
|
|
|
|
return roomCache[roomId][type];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === "timestamp") {
|
|
|
|
const ts = this._tsOfNewestEvent(room);
|
|
|
|
this._updateCachedRoomState(roomId, "timestamp", ts);
|
|
|
|
return ts;
|
|
|
|
} else if (type === "unread") {
|
|
|
|
const unread = room.getUnreadNotificationCount() > 0;
|
|
|
|
this._updateCachedRoomState(roomId, "unread", unread);
|
|
|
|
return unread;
|
|
|
|
} else if (type === "notifications") {
|
|
|
|
const notifs = room.getUnreadNotificationCount("highlight") > 0;
|
|
|
|
this._updateCachedRoomState(roomId, "notifications", notifs);
|
|
|
|
return notifs;
|
|
|
|
} else throw new Error("Unrecognized room cache type: " + type);
|
|
|
|
}
|
|
|
|
|
2018-02-16 14:16:50 +00:00
|
|
|
_eventTriggersRecentReorder(ev) {
|
|
|
|
return ev.getTs() && (
|
|
|
|
Unread.eventTriggersUnreadCount(ev) ||
|
|
|
|
ev.getSender() === this._matrixClient.credentials.userId
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-06 15:15:47 +00:00
|
|
|
_tsOfNewestEvent(room) {
|
|
|
|
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
|
|
|
const ev = room.timeline[i];
|
2018-02-16 14:16:50 +00:00
|
|
|
if (this._eventTriggersRecentReorder(ev)) {
|
2018-02-06 15:15:47 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-01 20:43:15 +00:00
|
|
|
_recentsComparator(roomA, roomB, pinUnread, pinMentioned) {
|
2018-10-12 20:35:54 +00:00
|
|
|
// We try and set the ordering to be Mentioned > Unread > Recent
|
2018-11-01 22:28:13 +00:00
|
|
|
// assuming the user has the right settings, of course.
|
|
|
|
|
|
|
|
const timestampA = this._getRoomState(roomA, "timestamp");
|
|
|
|
const timestampB = this._getRoomState(roomB, "timestamp");
|
|
|
|
const timestampDiff = timestampB - timestampA;
|
2018-10-12 20:35:54 +00:00
|
|
|
|
|
|
|
if (pinMentioned) {
|
2018-11-01 22:28:13 +00:00
|
|
|
const mentionsA = this._getRoomState(roomA, "notifications");
|
|
|
|
const mentionsB = this._getRoomState(roomB, "notifications");
|
|
|
|
if (mentionsA && !mentionsB) return -1;
|
|
|
|
if (!mentionsA && mentionsB) return 1;
|
|
|
|
|
|
|
|
// If they both have notifications, sort by timestamp.
|
|
|
|
// If neither have notifications (the fourth check not shown
|
|
|
|
// here), then try and sort by unread messages and finally by
|
|
|
|
// timestamp.
|
|
|
|
if (mentionsA && mentionsB) return timestampDiff;
|
2018-10-12 20:35:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (pinUnread) {
|
2018-11-01 22:28:13 +00:00
|
|
|
const unreadA = this._getRoomState(roomA, "unread");
|
2018-11-01 23:07:05 +00:00
|
|
|
const unreadB = this._getRoomState(roomB, "unread");
|
2018-10-12 20:35:54 +00:00
|
|
|
if (unreadA && !unreadB) return -1;
|
|
|
|
if (!unreadA && unreadB) return 1;
|
2018-11-01 22:28:13 +00:00
|
|
|
|
|
|
|
// If they both have unread messages, sort by timestamp
|
|
|
|
// If nether have unread message (the fourth check not shown
|
|
|
|
// here), then just sort by timestamp anyways.
|
|
|
|
if (unreadA && unreadB) return timestampDiff;
|
2018-10-12 20:35:54 +00:00
|
|
|
}
|
|
|
|
|
2018-11-01 22:28:13 +00:00
|
|
|
return timestampDiff;
|
2018-02-06 15:15:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_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
|
2018-08-23 11:05:09 +00:00
|
|
|
const a = metaA ? metaA.order : undefined;
|
|
|
|
const b = metaB ? metaB.order : undefined;
|
2018-02-06 15:15:47 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-01-25 21:16:03 +00:00
|
|
|
getRoomLists() {
|
|
|
|
return this._state.lists;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (global.singletonRoomListStore === undefined) {
|
|
|
|
global.singletonRoomListStore = new RoomListStore();
|
|
|
|
}
|
|
|
|
export default global.singletonRoomListStore;
|