Files
webmail/hooks/use-keyword-format.ts
T
lucamzanon 42b6cc08b0 feat: colour unified rows by account and give every tag a hue of its own
In the unified view each row can be shaded with its account colour
instead of carrying an account dot, behind a layout setting. Tags keep
their own generated hue on their chips.
2026-09-19 05:44:44 +02:00

75 lines
3.1 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";
/**
* 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.
*/
export function useKeywordFormat() {
const keywords = useSettingsStore((state) => state.emailKeywords);
const nested = useSettingsStore((state) => state.nestedTags);
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 color = keywords.find((keyword) => keyword.id === id)?.color;
if (color && KEYWORD_PALETTE[color]) return KEYWORD_PALETTE[color];
return KEYWORD_PALETTE[suggestKeywordColor(id)] ?? KEYWORD_PALETTE[FALLBACK_KEYWORD_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],
);
}