Files
ThreadNet-Web/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts
T
Florian DurosandGitHub 1ef544c5c6 Room list: add analytic events (#33625)
* chore: update analytics

* feat: add new analytics
2026-05-27 09:50:22 +00:00

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");
};
}