164 lines
6.0 KiB
TypeScript
164 lines
6.0 KiB
TypeScript
/*
|
|
* Copyright 2026 Element Creations Ltd.
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
* Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
|
import {
|
|
BaseViewModel,
|
|
type RoomListSectionHeaderActions,
|
|
type RoomListSectionHeaderViewSnapshot,
|
|
} from "@element-hq/web-shared-components";
|
|
|
|
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
|
import { NotificationStateEvents } from "../../stores/notifications/NotificationState";
|
|
import { type RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
|
|
import SettingsStore from "../../settings/SettingsStore";
|
|
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
|
import { getCustomSectionData, isCustomSectionTag, isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
|
import PosthogTrackers from "../../PosthogTrackers";
|
|
|
|
interface RoomListSectionHeaderViewModelProps {
|
|
tag: string;
|
|
title: string;
|
|
/**
|
|
* The ID of the current space.
|
|
*/
|
|
spaceId: string;
|
|
onToggleExpanded: (isExpanded: boolean) => void;
|
|
}
|
|
|
|
export class RoomListSectionHeaderViewModel
|
|
extends BaseViewModel<RoomListSectionHeaderViewSnapshot, RoomListSectionHeaderViewModelProps>
|
|
implements RoomListSectionHeaderActions
|
|
{
|
|
/**
|
|
* The notification states of the rooms currently in this section, used to compute the unread state.
|
|
*/
|
|
private roomNotificationStates = new Set<RoomNotificationState>();
|
|
|
|
/**
|
|
* Tracks the expanded/collapsed state per space.
|
|
* Key is spaceId. Defaults to expanded if not set.
|
|
*/
|
|
private readonly expandedBySpace = new Map<string, boolean>();
|
|
|
|
public constructor(props: RoomListSectionHeaderViewModelProps) {
|
|
const isDefaultSection = isDefaultSectionTag(props.tag);
|
|
super(props, {
|
|
id: props.tag,
|
|
title: props.title,
|
|
isExpanded: true,
|
|
isUnread: false,
|
|
displaySectionMenu: !isDefaultSection,
|
|
});
|
|
const sectionWatherRef = SettingsStore.watchSetting("RoomList.CustomSectionData", null, () =>
|
|
this.onCustomSectionDataChange(),
|
|
);
|
|
this.disposables.track(() => SettingsStore.unwatchSetting(sectionWatherRef));
|
|
}
|
|
|
|
public onClick = (): void => {
|
|
const isExpanded = !this.snapshot.current.isExpanded;
|
|
this.expandedBySpace.set(this.props.spaceId, isExpanded);
|
|
this.snapshot.merge({ isExpanded });
|
|
this.props.onToggleExpanded(isExpanded);
|
|
};
|
|
|
|
/**
|
|
* Whether the section is currently expanded or not.
|
|
*/
|
|
public get isExpanded(): boolean {
|
|
return this.snapshot.current.isExpanded;
|
|
}
|
|
|
|
/**
|
|
* Set whether the section is expanded for the current space.
|
|
* This will not trigger the onToggleExpanded callback.
|
|
*/
|
|
public set isExpanded(value: boolean) {
|
|
this.expandedBySpace.set(this.props.spaceId, value);
|
|
this.snapshot.merge({ isExpanded: value });
|
|
|
|
const kind = value ? "Expand" : "Collapse";
|
|
PosthogTrackers.trackCollapseOrExpandSection(kind, "SectionHeader");
|
|
}
|
|
|
|
/**
|
|
* Switch to a different space, restoring the expanded state for that space.
|
|
* Defaults to expanded if no state has been saved for the space.
|
|
*/
|
|
public setSpace(spaceId: string): void {
|
|
this.props.spaceId = spaceId;
|
|
const isExpanded = this.expandedBySpace.get(this.props.spaceId) ?? true;
|
|
this.snapshot.merge({ isExpanded });
|
|
}
|
|
|
|
/**
|
|
* Update the rooms tracked by this section header for unread state computation.
|
|
* Only subscribes to new rooms and unsubscribes from rooms no longer in the section.
|
|
* @param rooms - The rooms currently in this section
|
|
*/
|
|
public setRooms(rooms: Room[]): void {
|
|
const newStates = new Set(rooms.map((room) => RoomNotificationStateStore.instance.getRoomState(room)));
|
|
|
|
// Unsubscribe from rooms no longer in the section
|
|
for (const state of this.roomNotificationStates) {
|
|
if (!newStates.has(state)) {
|
|
state.off(NotificationStateEvents.Update, this.updateUnreadState);
|
|
}
|
|
}
|
|
|
|
// Subscribe to newly added rooms
|
|
for (const state of newStates) {
|
|
if (!this.roomNotificationStates.has(state)) {
|
|
// We don't use trackListener because we don't want to grow the disposables indefinitely as rooms are added and removed from the section
|
|
state.on(NotificationStateEvents.Update, this.updateUnreadState);
|
|
}
|
|
}
|
|
|
|
this.roomNotificationStates = newStates;
|
|
this.updateUnreadState();
|
|
}
|
|
|
|
/**
|
|
* Update the unread state of the section header based on the notification states of the tracked rooms.
|
|
*/
|
|
private updateUnreadState = (): void => {
|
|
const isUnread = [...this.roomNotificationStates].some((state) => state.hasAnyNotificationOrActivity);
|
|
this.snapshot.merge({ isUnread });
|
|
};
|
|
|
|
public dispose(): void {
|
|
for (const state of this.roomNotificationStates) {
|
|
state.off(NotificationStateEvents.Update, this.updateUnreadState);
|
|
}
|
|
this.roomNotificationStates.clear();
|
|
super.dispose();
|
|
}
|
|
|
|
/**
|
|
* Handle changes to custom section data.
|
|
*/
|
|
private onCustomSectionDataChange(): void {
|
|
const sectionData = isCustomSectionTag(this.props.tag) ? getCustomSectionData()[this.props.tag] : undefined;
|
|
if (sectionData) {
|
|
this.snapshot.merge({ title: sectionData.name });
|
|
}
|
|
}
|
|
|
|
public editSection = async (): Promise<void> => {
|
|
await RoomListStoreV3.instance.editSection(this.props.tag);
|
|
};
|
|
|
|
public removeSection = async (): Promise<void> => {
|
|
// There is one notification state per room in the section
|
|
const isEmpty = this.roomNotificationStates.size === 0;
|
|
await RoomListStoreV3.instance.removeSection(this.props.tag, isEmpty);
|
|
|
|
PosthogTrackers.trackInteraction("WebDeleteSection");
|
|
};
|
|
}
|