fae7af1a62
Previously we assumed that a decrypted event has a room_id but this isn't necessarily true for to_device events. It makes sense to ignore events that aren't associated with rooms anyway given that the list we're updating only contains rooms!
302 lines
11 KiB
JavaScript
302 lines
11 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';
|
|
|
|
/**
|
|
* 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: {
|
|
"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.RoomMember.membership': {
|
|
if (!this._matrixClient || payload.member.userId !== this._matrixClient.credentials.userId) break;
|
|
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 = {
|
|
"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");
|
|
}
|
|
});
|
|
|
|
Object.keys(lists).forEach((listKey) => {
|
|
let comparator;
|
|
switch (RoomListStore._listOrders[listKey]) {
|
|
case "recent":
|
|
comparator = this._recentsComparator;
|
|
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) {
|
|
// 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.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;
|