element-web/src/stores/BreadcrumbsStore.ts

189 lines
7.6 KiB
TypeScript
Raw Normal View History

/*
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> {
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 19:13:39 +00:00
private static readonly internalInstance = (() => {
const instance = new BreadcrumbsStore();
instance.start();
return instance;
})();
2022-12-12 11:24:14 +00:00
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) {
2022-12-12 11:24:14 +00:00
if (payload.settingName === "breadcrumb_rooms") {
await this.updateRooms();
2022-12-12 11:24:14 +00:00
} else if (payload.settingName === "breadcrumbs" || payload.settingName === "feature_breadcrumbs_v2") {
2021-06-29 12:11:58 +00:00
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.
2021-06-29 12:11:58 +00:00
this.waitingRooms.push({ roomId: payload.room_id, addedTs: Date.now() });
} else {
2020-06-09 00:26:43 +00:00
// The tests might not result in a valid room object.
const room = this.matrixClient.getRoom(payload.room_id);
const membership = room?.getMyMembership();
2022-12-12 11:24:14 +00:00
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();
2021-06-29 12:11:58 +00:00
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.
2022-12-12 11:24:14 +00:00
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> => {
2022-12-12 11:24:14 +00:00
const waitingRoom = this.waitingRooms.find((r) => r.roomId === room.roomId);
if (!waitingRoom) return;
this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1);
2022-12-12 11:24:14 +00:00
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 = [];
2022-12-12 11:24:14 +00:00
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)
2021-06-29 12:11:58 +00:00
await this.updateState({ rooms });
}
private async appendRoom(room: Room): Promise<void> {
2020-07-09 00:31:44 +00:00
let updated = false;
2020-06-09 00:18:34 +00:00
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++) {
2022-12-12 11:24:14 +00:00
const idx = rooms.findIndex((r) => r.roomId === history[i].roomId);
2020-07-09 00:31:44 +00:00
if (idx !== -1) {
rooms.splice(idx, 1);
updated = true;
}
}
}
// Remove the existing room, if it is present
2022-12-12 11:24:14 +00:00
const existingIdx = rooms.findIndex((r) => r.roomId === room.roomId);
2020-07-09 00:31:44 +00:00
// 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);
2020-07-09 00:31:44 +00:00
updated = true;
}
2020-07-09 00:31:44 +00:00
if (updated) {
// Update the breadcrumbs
2021-06-29 12:11:58 +00:00
await this.updateState({ rooms });
2022-12-12 11:24:14 +00:00
const roomIds = rooms.map((r) => r.roomId);
2020-07-09 00:31:44 +00:00
if (roomIds.length > 0) {
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
}
}