Files
webmail/hooks/use-keyword-format.ts
T

88 lines
3.8 KiB
TypeScript

"use client";
import { useMemo } from "react";
import {
useSettingsStore,
KEYWORD_PALETTE,
FALLBACK_KEYWORD_COLOR,
type KeywordColor,
} from "@/stores/settings-store";
import { formatKeyword, formatKeywordLabels, keywordRenderings } from "@/lib/keyword-format";
import { suggestKeywordColor } from "@/lib/keyword-discovery";
import { useAccountTags, type TagScope } from "@/hooks/use-account-tags";
import { CVD_CLASSES, cvdHue } from "@/lib/cvd-palette";
/**
* Names and colours tags for the screen, bound to the user's tag settings.
*
* Resolving the definitions and the nesting setting here rather than at every
* call site means no caller can forget the setting and render a nested name to
* someone who never asked for nesting. Subscribing to them also keeps tags in
* step the moment either changes: reading the store inside the formatter would
* leave every list stale until something else happened to re-render it.
*
* The definitions are those of the account in `scope` (useAccountTags): a
* group account's shared tags, otherwise the user's own. Without `scope`
* the TagScopeContext around the caller decides.
*/
export function useKeywordFormat(scope?: TagScope | null) {
const keywords = useAccountTags(scope).definitions;
const nested = useSettingsStore((state) => state.nestedTags);
const colorBlindPalette = useSettingsStore((state) => state.colorBlindPalette);
return useMemo(
() => ({
/** The tag's display name. */
tagName: (id: string) => formatKeyword(id, keywords, nested),
/** Its progressively shorter forms, longest first, for `useShortenedText`. */
tagNameCandidates: (id: string) => keywordRenderings(formatKeywordLabels(id, keywords, nested)),
/**
* The tag's colour.
*
* A keyword this client has no definition for - one created on another
* device, applied by a server-side filter, or whose tag was deleted -
* still gets a hue of its own, derived from its id by the same function
* that proposes colours for recovered tags. One shared grey made every
* such tag look alike, which is exactly when telling them apart matters:
* a mailbox filling up with server-assigned tags would show a single
* undifferentiated colour. The hue is a pure function of the id, so it is
* stable across reloads and devices, and it degrades to grey only when
* the proposal names a colour the palette does not hold.
*/
tagColor: (id: string): KeywordColor => {
const stored = keywords.find((keyword) => keyword.id === id)?.color;
const suggested = suggestKeywordColor(id);
const color = stored && KEYWORD_PALETTE[stored]
? stored
: KEYWORD_PALETTE[suggested] ? suggested : FALLBACK_KEYWORD_COLOR;
// Colour-blind-safe colours: the same tag, drawn in the palette
// colour nearest its own (lib/cvd-palette.ts). The stored colour stays.
if (colorBlindPalette) return CVD_CLASSES[cvdHue(color) ?? "grey"];
return KEYWORD_PALETTE[color];
},
/**
* Tag ids in the order the user arranged them in settings.
*
* The keywords on a message arrive as an unordered JMAP map, so without
* this the same two tags can swap places between rows. A tag with no
* local definition has no place in that order, so it sorts last, by name.
*/
sortTagIds: (ids: string[]): string[] => {
const rank = (id: string) => {
const index = keywords.findIndex((keyword) => keyword.id === id);
return index === -1 ? keywords.length : index;
};
return [...ids].sort(
(a, b) =>
rank(a) - rank(b) ||
formatKeyword(a, keywords, nested).localeCompare(formatKeyword(b, keywords, nested)),
);
},
}),
[keywords, nested, colorBlindPalette],
);
}