import { MatrixClient } from 'matrix-js-sdk'; /** * [Gitea #108] User-defined sidebar sections from standard `u.` room * tags, so they sync across devices and other clients see the same tags. */ export const SECTION_TAG_PREFIX = 'u.'; export type RoomSection = { /** Display name ("Raids"). */ name: string; /** Full tag name ("u.Raids"). */ tag: string; /** Member room ids, by the tag's `order` (ascending), then by name. */ rooms: string[]; }; export type TaggedRoom = { roomId: string; name: string; tags: Record | undefined; }; export const sectionTag = (name: string): string => `${SECTION_TAG_PREFIX}${name.trim()}`; export const isSectionTag = (tag: string): boolean => tag.startsWith(SECTION_TAG_PREFIX) && tag.length > SECTION_TAG_PREFIX.length; export const sectionName = (tag: string): string => tag.slice(SECTION_TAG_PREFIX.length); const orderOf = (v: { order?: number | string } | undefined): number => { const n = typeof v?.order === 'string' ? parseFloat(v.order) : v?.order; return typeof n === 'number' && Number.isFinite(n) ? n : 1; }; /** * Sections (alphabetical by name) with their members ordered by tag `order` * then name; `sectioned` is the set of rooms that appear in any section. * Empty sections simply don't exist — a section is its members' tags. */ export function deriveRoomSections(rooms: TaggedRoom[]): { sections: RoomSection[]; sectioned: Set; } { const byTag = new Map(); const sectioned = new Set(); rooms.forEach((room) => { Object.entries(room.tags ?? {}).forEach(([tag, value]) => { if (!isSectionTag(tag)) return; const list = byTag.get(tag) ?? []; list.push({ roomId: room.roomId, order: orderOf(value), name: room.name }); byTag.set(tag, list); sectioned.add(room.roomId); }); }); const sections = [...byTag.entries()] .map(([tag, members]) => ({ tag, name: sectionName(tag), rooms: members .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name)) .map((m) => m.roomId), })) .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); return { sections, sectioned }; } /** Every section name in use across the user's rooms (for the "Add to section" menu). */ export function listSectionNames(mx: MatrixClient): string[] { const names = new Set(); mx.getRooms().forEach((room) => { Object.keys(room.tags ?? {}).forEach((tag) => { if (isSectionTag(tag)) names.add(sectionName(tag)); }); }); return [...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); } /** Validate a new section name; returns the error to show, or undefined. */ export const validateSectionName = (raw: string, existing: string[]): string | undefined => { const name = raw.trim(); if (!name) return 'Give the section a name.'; if (name.length > 40) return 'Keep it under 40 characters.'; if (name.includes('.')) return 'Dots are not allowed in section names.'; if (existing.some((e) => e.toLowerCase() === name.toLowerCase())) return 'A section with that name already exists.'; return undefined; };