This repository was archived by the owner on Oct 22, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathBreadcrumbsStore.ts
190 lines (160 loc) · 7.71 KB
/
BreadcrumbsStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Room, RoomEvent, ClientEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import SettingsStore from "../settings/SettingsStore";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff, filterBoolean } from "../utils/arrays";
import { SettingLevel } from "../settings/SettingLevel";
import { Action } from "../dispatcher/actions";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomPayload } from "../dispatcher/payloads/JoinRoomPayload";
const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
interface IState {
enabled?: boolean;
rooms?: Room[];
}
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
private static readonly internalInstance = (() => {
const instance = new BreadcrumbsStore();
instance.start();
return instance;
})();
private waitingRooms: { roomId: string; addedTs: number }[] = [];
private constructor() {
super(defaultDispatcher);
SettingsStore.monitorSetting("breadcrumb_rooms", null);
SettingsStore.monitorSetting("breadcrumbs", null);
}
public static get instance(): BreadcrumbsStore {
return BreadcrumbsStore.internalInstance;
}
public get rooms(): Room[] {
return this.state.rooms || [];
}
public get visible(): boolean {
return !!this.state.enabled && this.meetsRoomRequirement;
}
/**
* Do we have enough rooms to justify showing the breadcrumbs?
* (Or is the labs feature enabled?)
*
* @returns true if there are at least 20 visible rooms.
*/
public get meetsRoomRequirement(): boolean {
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
return !!this.matrixClient && this.matrixClient.getVisibleRooms(msc3946ProcessDynamicPredecessor).length >= 20;
}
protected async onAction(payload: SettingUpdatedPayload | ViewRoomPayload | JoinRoomPayload): Promise<void> {
if (!this.matrixClient) return;
if (payload.action === Action.SettingUpdated) {
if (payload.settingName === "breadcrumb_rooms") {
await this.updateRooms();
} else if (payload.settingName === "breadcrumbs") {
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
}
} else if (payload.action === Action.ViewRoom) {
if (payload.auto_join && payload.room_id && !this.matrixClient.getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately. We're probably just
// waiting for a room join to complete.
this.waitingRooms.push({ roomId: payload.room_id, addedTs: Date.now() });
} else {
// The tests might not result in a valid room object.
const room = this.matrixClient.getRoom(payload.room_id);
const membership = room?.getMyMembership();
if (room && membership === KnownMembership.Join) await this.appendRoom(room);
}
} else if (payload.action === Action.JoinRoom) {
const room = this.matrixClient.getRoom(payload.roomId);
if (room) await this.appendRoom(room);
}
}
protected async onReady(): Promise<void> {
await this.updateRooms();
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
if (this.matrixClient) {
this.matrixClient.on(RoomEvent.MyMembership, this.onMyMembership);
this.matrixClient.on(ClientEvent.Room, this.onRoom);
}
}
protected async onNotReady(): Promise<void> {
if (this.matrixClient) {
this.matrixClient.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.matrixClient.removeListener(ClientEvent.Room, this.onRoom);
}
}
private onMyMembership = async (room: Room): Promise<void> => {
// Only turn on breadcrumbs is the user hasn't explicitly turned it off again.
const settingValueRaw = SettingsStore.getValue("breadcrumbs", null, /*excludeDefault=*/ true);
if (this.meetsRoomRequirement && isNullOrUndefined(settingValueRaw)) {
await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
}
};
private onRoom = async (room: Room): Promise<void> => {
const waitingRoom = this.waitingRooms.find((r) => r.roomId === room.roomId);
if (!waitingRoom) return;
this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1);
if (Date.now() - waitingRoom.addedTs > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
await this.appendRoom(room);
};
private async updateRooms(): Promise<void> {
let roomIds = SettingsStore.getValue<string[]>("breadcrumb_rooms");
if (!roomIds || roomIds.length === 0) roomIds = [];
const rooms = filterBoolean(roomIds.map((r) => this.matrixClient?.getRoom(r)));
const currentRooms = this.state.rooms || [];
if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo)
await this.updateState({ rooms });
}
private async appendRoom(room: Room): Promise<void> {
let updated = false;
const rooms = (this.state.rooms || []).slice(); // cheap clone
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor);
if (history && history.length > 1) {
room = history[history.length - 1]; // Last room is most recent in history
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex((r) => r.roomId === history[i].roomId);
if (idx !== -1) {
rooms.splice(idx, 1);
updated = true;
}
}
}
// Remove the existing room, if it is present
const existingIdx = rooms.findIndex((r) => r.roomId === room.roomId);
// If we're focusing on the first room no-op
if (existingIdx !== 0) {
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
// Splice the room to the start of the list
rooms.splice(0, 0, room);
updated = true;
}
if (rooms.length > MAX_ROOMS) {
// This looks weird, but it's saying to start at the MAX_ROOMS point in the
// list and delete everything after it.
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
updated = true;
}
if (updated) {
// Update the breadcrumbs
await this.updateState({ rooms });
const roomIds = rooms.map((r) => r.roomId);
if (roomIds.length > 0) {
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
}
}