334 lines
13 KiB
JavaScript
334 lines
13 KiB
JavaScript
/*
|
|
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';
|
|
import SettingsStore from "../settings/SettingsStore";
|
|
|
|
/**
|
|
* A class for storing application state for categorising rooms in
|
|
* the RoomList.
|
|
*/
|
|
class RoomListStore extends Store {
|
|
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",
|
|
};
|
|
|
|
constructor() {
|
|
super(dis);
|
|
|
|
this._init();
|
|
this._getManualComparator = this._getManualComparator.bind(this);
|
|
this._recentsComparator = this._recentsComparator.bind(this);
|
|
}
|
|
|
|
_init() {
|
|
// Initialise state
|
|
this._state = {
|
|
lists: {
|
|
"m.server_notice": [],
|
|
"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.Room.timeline': {
|
|
if (!this._state.ready ||
|
|
!payload.isLiveEvent ||
|
|
!payload.isLiveUnfilteredRoomTimelineEvent ||
|
|
!this._eventTriggersRecentReorder(payload.event)
|
|
) break;
|
|
this._generateRoomLists();
|
|
}
|
|
break;
|
|
// 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': {
|
|
// 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;
|
|
|
|
const liveTimeline = room.getLiveTimeline();
|
|
const eventTimeline = room.getTimelineForEvent(payload.event.getId());
|
|
|
|
// 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 ||
|
|
!this._eventTriggersRecentReorder(payload.event)
|
|
) break;
|
|
this._generateRoomLists();
|
|
}
|
|
break;
|
|
case 'MatrixActions.accountData': {
|
|
if (payload.event_type !== 'm.direct') break;
|
|
this._generateRoomLists();
|
|
}
|
|
break;
|
|
case 'MatrixActions.Room.myMembership': {
|
|
this._generateRoomLists();
|
|
}
|
|
break;
|
|
// 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;
|
|
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 = {
|
|
"m.server_notice": [],
|
|
"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 myUserId = this._matrixClient.getUserId();
|
|
const membership = room.getMyMembership();
|
|
const me = room.getMember(myUserId);
|
|
|
|
if (membership == "invite") {
|
|
lists["im.vector.fake.invite"].push(room);
|
|
} else if (membership == "join" || membership === "ban" || (me && me.isKicked())) {
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// ignore any m. tag names we don't know about
|
|
tagNames = tagNames.filter((t) => {
|
|
return !t.startsWith('m.') || lists[t] !== undefined;
|
|
});
|
|
|
|
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 (membership === "leave") {
|
|
lists["im.vector.fake.archived"].push(room);
|
|
}
|
|
});
|
|
|
|
// 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");
|
|
Object.keys(lists).forEach((listKey) => {
|
|
let comparator;
|
|
switch (RoomListStore._listOrders[listKey]) {
|
|
case "recent":
|
|
comparator = (roomA, roomB) => {
|
|
return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned);
|
|
};
|
|
break;
|
|
case "manual":
|
|
default:
|
|
comparator = this._getManualComparator(listKey, optimisticRequest);
|
|
break;
|
|
}
|
|
lists[listKey].sort(comparator);
|
|
});
|
|
|
|
this._setState({
|
|
lists,
|
|
ready: true, // Ready to receive updates via Room.tags events
|
|
});
|
|
}
|
|
|
|
_eventTriggersRecentReorder(ev) {
|
|
return ev.getTs() && (
|
|
Unread.eventTriggersUnreadCount(ev) ||
|
|
ev.getSender() === this._matrixClient.credentials.userId
|
|
);
|
|
}
|
|
|
|
_tsOfNewestEvent(room) {
|
|
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
|
const ev = room.timeline[i];
|
|
if (this._eventTriggersRecentReorder(ev)) {
|
|
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, pinUnread, pinMentioned) {
|
|
// We try and set the ordering to be Mentioned > Unread > Recent
|
|
// assuming the user has the right settings, of course
|
|
|
|
if (pinMentioned) {
|
|
const mentionsA = roomA.getUnreadNotificationCount("highlight") > 0;
|
|
const mentionsB = roomB.getUnreadNotificationCount("highlight") > 0;
|
|
if (mentionsA && !mentionsB) return -1;
|
|
if (!mentionsA && mentionsB) return 1;
|
|
if (mentionsA && mentionsB) return 0;
|
|
// If neither have mentions, fall through to remaining checks
|
|
}
|
|
|
|
if (pinUnread) {
|
|
const unreadA = Unread.doesRoomHaveUnreadMessages(roomA);
|
|
const unreadB = Unread.doesRoomHaveUnreadMessages(roomB);
|
|
if (unreadA && !unreadB) return -1;
|
|
if (!unreadA && unreadB) return 1;
|
|
if (unreadA && unreadB) return 0;
|
|
// If neither have unread messages, fall through to remaining checks
|
|
}
|
|
|
|
// XXX: We could use a cache here and update it when we see new
|
|
// events that trigger a reorder
|
|
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 ? metaA.order : undefined;
|
|
const b = metaB ? metaB.order : undefined;
|
|
|
|
// 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;
|