87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
import { MatrixClient } from 'matrix-js-sdk';
|
|||
|
|
|
||
|
|
/**
|
||
|
|
* [Gitea #108] User-defined sidebar sections from standard `u.<name>` 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<string, { order?: number | string } | undefined> | 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<string>;
|
||
|
|
} {
|
||
|
|
const byTag = new Map<string, { roomId: string; order: number; name: string }[]>();
|
||
|
|
const sectioned = new Set<string>();
|
||
|
|
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<string>();
|
||
|
|
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;
|
||
|
|
};
|