element-web/src/stores/BreadcrumbsStore.ts
Michael Telatynski da7aa4055e
Conform more of the code base to strict null checking (#10147)
* Conform more of the code base to strict null checking

* More strict fixes

* More strict work

* Fix missing optional type

* Iterate
2023-02-13 17:01:43 +00:00

188 lines
7.6 KiB
TypeScript

/*
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
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 { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { ClientEvent } from "matrix-js-sdk/src/client";
import SettingsStore from "../settings/SettingsStore";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } 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);
SettingsStore.monitorSetting("feature_breadcrumbs_v2", 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;
}
public get meetsRoomRequirement(): boolean {
if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true;
return this.matrixClient?.getVisibleRooms().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" || payload.settingName === "feature_breadcrumbs_v2") {
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
}
} else if (payload.action === Action.ViewRoom) {
if (payload.auto_join && !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 === "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) });
this.matrixClient.on(RoomEvent.MyMembership, this.onMyMembership);
this.matrixClient.on(ClientEvent.Room, this.onRoom);
}
protected async onNotReady(): Promise<void> {
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 = roomIds.map((r) => this.matrixClient.getRoom(r)).filter((r) => !!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
// 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);
if (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);
}
}
}
}