1/** Ordering and promotion rules for saved replies; import-free for the check file. */
2
3export interface SavedReplyUse {
4 /** Stored with the count so root rows render without fetching the list. */
5 title: string;
6 count: number;
7}
8
9export type SavedReplyUsage = Record<string, SavedReplyUse>;
10
11/** Orders the drill-down on an empty query; once typed, scoring decides and this
12 * is the tie-breaker. Stable sort keeps never-used replies in `modified desc`. */
13export function mostUsedFirst<T extends { name: string }>(
14 replies: T[],
15 usage: SavedReplyUsage
16): T[] {
17 const count = (name: string) => usage[name]?.count ?? 0;
18 return [...replies].sort((a, b) => count(b.name) - count(a.name));
19}
20
21/** Root-row promotion. The threshold makes long-tail usage degrade to nothing
22 * rather than to noise. */
23export function topSavedReplies(
24 usage: SavedReplyUsage,
25 minUses: number,
26 maxRows: number
27): Array<SavedReplyUse & { name: string }> {
28 return Object.entries(usage)
29 .filter(([, use]) => use.count >= minUses)
30 .sort((a, b) => b[1].count - a[1].count)
31 .slice(0, maxRows)
32 .map(([name, use]) => ({ name, ...use }));
33}
34
35export function withUse(
36 usage: SavedReplyUsage,
37 replyId: string,
38 title: string
39): SavedReplyUsage {
40 const count = (usage[replyId]?.count ?? 0) + 1;
41 return { ...usage, [replyId]: { title, count } };
42}
43
44/** Drops a reply that no longer exists, so its root row cannot fail forever. */
45export function withoutReply(
46 usage: SavedReplyUsage,
47 replyId: string
48): SavedReplyUsage {
49 const { [replyId]: _gone, ...rest } = usage;
50 return rest;
51}
52