mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
88 lines
3.8 KiB
TypeScript
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],
|
|
);
|
|
}
|