Room list: add robustness to custom section loading (#33475)
* refactor: move all access to custom sections settings into `section.ts` * fix: add robustness when getting the order list of custom sections * fix: add robustness when getting the custom section data * fix: ignore malformed section but don't erase them * fix: remove useless await operator * test: add more tests
This commit is contained in:
@@ -40,7 +40,7 @@ import { DefaultTagID } from "./skip-list/tag";
|
||||
import { ExcludeTagsFilter } from "./skip-list/filters/ExcludeTagsFilter";
|
||||
import { TagFilter } from "./skip-list/filters/TagFilter";
|
||||
import { filterBoolean } from "../../utils/arrays";
|
||||
import { CHATS_TAG, createSection, deleteSection, editSection } from "./section";
|
||||
import { CHATS_TAG, createSection, deleteSection, editSection, getOrderedCustomSections } from "./section";
|
||||
|
||||
/**
|
||||
* These are the filters passed to the room skip list.
|
||||
@@ -522,7 +522,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
* Load the custom sections from the settings store and update the sorted tags.
|
||||
*/
|
||||
private loadCustomSections(): void {
|
||||
const orderedCustomSections = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
||||
const orderedCustomSections = getOrderedCustomSections();
|
||||
this.sortedTags = [DefaultTagID.Favourite, ...orderedCustomSections, CHATS_TAG, DefaultTagID.LowPriority];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectio
|
||||
import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog";
|
||||
import { DefaultTagID, type TagID } from "./skip-list/tag";
|
||||
|
||||
type Tag = string;
|
||||
|
||||
/**
|
||||
* A synthetic tag used to represent the "Chats" section, which contains
|
||||
* every room that does not belong to any other explicit tag section.
|
||||
@@ -27,12 +25,14 @@ export const CHATS_TAG = "chats";
|
||||
*/
|
||||
export const CUSTOM_SECTION_TAG_PREFIX = "element.io.section.";
|
||||
|
||||
type CustomTag = `${typeof CUSTOM_SECTION_TAG_PREFIX}${string}`;
|
||||
|
||||
/**
|
||||
* Checks if a given tag is a custom section tag.
|
||||
* @param tag - The tag to check.
|
||||
* @returns True if the tag is a custom section tag, false otherwise.
|
||||
*/
|
||||
export function isCustomSectionTag(tag: string): boolean {
|
||||
export function isCustomSectionTag(tag: string): tag is CustomTag {
|
||||
return tag.startsWith(CUSTOM_SECTION_TAG_PREFIX);
|
||||
}
|
||||
|
||||
@@ -58,18 +58,56 @@ export function isSectionTag(tagId: TagID): boolean {
|
||||
* Structure of the custom section stored in the settings. The tag is used as a unique identifier for the section, and the name is given by the user.
|
||||
*/
|
||||
type CustomSection = {
|
||||
tag: Tag;
|
||||
tag: CustomTag;
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a valid CustomSection object.
|
||||
*/
|
||||
function isValidCustomSection(value: unknown): value is CustomSection {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
isCustomSectionTag((value as Record<string, unknown>).tag as string) &&
|
||||
typeof (value as Record<string, unknown>).name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The custom sections data is stored as a record in the settings, where the key is the section tag and the value is the section data (name and tag).
|
||||
*/
|
||||
export type CustomSectionsData = Record<Tag, CustomSection>;
|
||||
export type CustomSectionsData = Record<CustomTag, CustomSection>;
|
||||
/**
|
||||
* Ordered list of custom section tags.
|
||||
*/
|
||||
export type OrderedCustomSections = Tag[];
|
||||
export type OrderedCustomSections = CustomTag[];
|
||||
|
||||
/**
|
||||
* Retrieves the custom sections data from the settings.
|
||||
* Invalid or malformed entries are dropped and the cleaned data is persisted back to settings.
|
||||
*/
|
||||
export function getCustomSectionData(): CustomSectionsData {
|
||||
const raw = SettingsStore.getValue("RoomList.CustomSectionData");
|
||||
// Data are malformed
|
||||
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
||||
|
||||
// Filter out invalid entries
|
||||
return Object.fromEntries(
|
||||
Object.entries(raw).filter(([key, value]) => isValidCustomSection(value) && value.tag === key),
|
||||
) as CustomSectionsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ordered list of custom section tags from the settings.
|
||||
* If the settings contain tags that are not present in the custom section data, they will be filtered out and the settings will be updated to remove the unknown tags.
|
||||
*/
|
||||
export function getOrderedCustomSections(): OrderedCustomSections {
|
||||
const sectionData = getCustomSectionData();
|
||||
const rawValue = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
||||
const orderedSections: OrderedCustomSections = Array.isArray(rawValue) ? rawValue : [];
|
||||
return orderedSections.filter((tag) => tag in sectionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom section by showing a dialog to the user to enter the section name.
|
||||
@@ -83,16 +121,16 @@ export async function createSection(): Promise<string | undefined> {
|
||||
const [shouldCreateSection, sectionName] = await modal.finished;
|
||||
if (!shouldCreateSection || !sectionName) return undefined;
|
||||
|
||||
const tag = `${CUSTOM_SECTION_TAG_PREFIX}${window.crypto.randomUUID()}`;
|
||||
const tag: CustomTag = `${CUSTOM_SECTION_TAG_PREFIX}${window.crypto.randomUUID()}`;
|
||||
const newSection: CustomSection = { tag, name: sectionName };
|
||||
|
||||
// Save the new section data
|
||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
||||
const sectionData = getCustomSectionData();
|
||||
sectionData[tag] = newSection;
|
||||
await SettingsStore.setValue("RoomList.CustomSectionData", null, SettingLevel.ACCOUNT, sectionData);
|
||||
|
||||
// Add the new section to the ordered list of sections
|
||||
const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections") || [];
|
||||
const orderedSections = getOrderedCustomSections();
|
||||
orderedSections.push(tag);
|
||||
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, orderedSections);
|
||||
return tag;
|
||||
@@ -103,7 +141,11 @@ export async function createSection(): Promise<string | undefined> {
|
||||
* @param tag - The tag of the section to edit.
|
||||
*/
|
||||
export async function editSection(tag: string): Promise<void> {
|
||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
||||
if (!isCustomSectionTag(tag)) {
|
||||
logger.info("Unknown section tag, cannot edit section", tag);
|
||||
return;
|
||||
}
|
||||
const sectionData = getCustomSectionData();
|
||||
const section = sectionData[tag];
|
||||
if (!section) {
|
||||
logger.info("Unknown section tag, cannot edit section", tag);
|
||||
@@ -127,7 +169,11 @@ export async function editSection(tag: string): Promise<void> {
|
||||
* @param isEmpty - Whether the section is empty (has no rooms). If the section is not empty, the confirmation dialog will show a warning message.
|
||||
*/
|
||||
export async function deleteSection(tag: string, isEmpty: boolean): Promise<void> {
|
||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData");
|
||||
if (!isCustomSectionTag(tag)) {
|
||||
logger.info("Unknown section tag, cannot delete section", tag);
|
||||
return;
|
||||
}
|
||||
const sectionData = getCustomSectionData();
|
||||
if (!sectionData[tag]) {
|
||||
logger.info("Unknown section tag, cannot delete section", tag);
|
||||
return;
|
||||
@@ -138,8 +184,7 @@ export async function deleteSection(tag: string, isEmpty: boolean): Promise<void
|
||||
if (!shouldRemoveSection) return;
|
||||
|
||||
// Remove the section from the ordered list of sections
|
||||
const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
||||
const newOrderedSections = orderedSections.filter((sectionTag) => sectionTag !== tag);
|
||||
const newOrderedSections = getOrderedCustomSections().filter((sectionTag) => sectionTag !== tag);
|
||||
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, newOrderedSections);
|
||||
|
||||
// Remove the section data
|
||||
|
||||
@@ -39,8 +39,8 @@ import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { type Call, CallEvent } from "../../models/Call";
|
||||
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { getCustomSectionData, isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||
|
||||
interface RoomItemProps {
|
||||
room: Room;
|
||||
@@ -424,7 +424,7 @@ export class RoomListItemViewModel
|
||||
* Order follows the canonical section order from RoomListStoreV3.
|
||||
*/
|
||||
private static buildSections(roomTags: Room["tags"]): Section[] {
|
||||
const customSectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
||||
const customSectionData = getCustomSectionData();
|
||||
|
||||
return (
|
||||
RoomListStoreV3.instance.orderedSectionTags
|
||||
|
||||
@@ -17,7 +17,7 @@ import { NotificationStateEvents } from "../../stores/notifications/Notification
|
||||
import { type RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||
import { getCustomSectionData, isCustomSectionTag, isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||
|
||||
interface RoomListSectionHeaderViewModelProps {
|
||||
tag: string;
|
||||
@@ -139,8 +139,7 @@ export class RoomListSectionHeaderViewModel
|
||||
* Handle changes to custom section data.
|
||||
*/
|
||||
private onCustomSectionDataChange(): void {
|
||||
const customSectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
||||
const sectionData = customSectionData[this.props.tag];
|
||||
const sectionData = isCustomSectionTag(this.props.tag) ? getCustomSectionData()[this.props.tag] : undefined;
|
||||
if (sectionData) {
|
||||
this.snapshot.merge({ title: sectionData.name });
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ import { hasCreateRoomRights } from "./utils";
|
||||
import { keepIfSame } from "../../utils/keepIfSame";
|
||||
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||
import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel";
|
||||
import { getCustomSectionData, isCustomSectionTag, CHATS_TAG } from "../../stores/room-list-v3/section";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { tagRoom } from "../../utils/room/tagRoom";
|
||||
import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom";
|
||||
import { CHATS_TAG } from "../../stores/room-list-v3/section";
|
||||
|
||||
/**
|
||||
* Tracks the position of the active room within a specific section.
|
||||
@@ -287,8 +287,7 @@ export class RoomListViewModel
|
||||
public getSectionHeaderViewModel(tag: string): RoomListSectionHeaderViewModel {
|
||||
if (this.roomSectionHeaderViewModels.has(tag)) return this.roomSectionHeaderViewModels.get(tag)!;
|
||||
|
||||
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
|
||||
const title = TAG_TO_TITLE_MAP[tag] || customSections[tag]?.name || tag;
|
||||
const title = TAG_TO_TITLE_MAP[tag] || (isCustomSectionTag(tag) && getCustomSectionData()[tag]?.name) || tag;
|
||||
const viewModel = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title,
|
||||
@@ -692,11 +691,13 @@ function computeSections(
|
||||
roomsResult: RoomsResult,
|
||||
isSectionExpanded: (tag: string) => boolean,
|
||||
): { sections: Section[]; isFlatList: boolean } {
|
||||
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
|
||||
const customSections = getCustomSectionData();
|
||||
|
||||
const sections = roomsResult.sections
|
||||
// Only include sections that have rooms or are custom sections (which may be empty but should still be shown)
|
||||
.filter((section) => section.rooms.length > 0 || customSections[section.tag])
|
||||
.filter(
|
||||
(section) => section.rooms.length > 0 || (isCustomSectionTag(section.tag) && customSections[section.tag]),
|
||||
)
|
||||
// Remove roomIds for sections that are currently collapsed according to their section header view model
|
||||
.map((section) => ({
|
||||
...section,
|
||||
|
||||
Reference in New Issue
Block a user