activity: select-only multi-client filter

the freetype client field is gone. the picker is a select-only menu of known clients with multi-select, chips for the active set, and a 32-client cap shared with the server. the api accepts a comma-separated client list and filters any-of with bound parameters. the trigger carets come from phosphor icons, newly adopted. bundle budget rises to 900000 bytes for the picker and the icon dependency.
This commit is contained in:
2026-08-31 17:40:07 +02:00
parent d79dd0bbcb
commit b774b05456
12 changed files with 1093 additions and 130 deletions
+32 -1
View File
@@ -57,6 +57,37 @@ export function validateText(value: unknown): string | undefined {
return trimmed === "" ? undefined : trimmed;
}
/** `queries_repo.max_clients`: past this the API answers 400 rather than filter. */
export const MAX_CLIENTS = 32;
/**
* The client filter: a comma-separated list of exact addresses, canonicalized
* here and nowhere else.
*
* This is the one place the value is read, so it is the one place it can be made
* to mean exactly one thing. Everything downstream — the chips that show the
* filter and the request that applies it — reads what this returns, so the two
* cannot disagree about a link somebody pasted.
*
* The rule: entries are trimmed, blanks are dropped, repeats are dropped, and
* the list is cut to the cap. A blank entry names no client and a repeat asks
* for the same client twice, so neither changes which rows come back; dropping
* them is not a different filter, it is the same filter written once. The cut is
* a different filter, and it is the honest one available: the API refuses a
* longer list outright, so keeping the extra addresses would show a filter that
* cannot be applied at all.
*/
export function validateClients(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const seen = new Set<string>();
for (const entry of value.split(",")) {
const trimmed = entry.trim();
if (trimmed !== "") seen.add(trimmed);
if (seen.size === MAX_CLIENTS) break;
}
return seen.size === 0 ? undefined : [...seen].join(",");
}
/**
* The API filter for a validated search, built field by field.
*
@@ -82,7 +113,7 @@ export function validateActivitySearch(search: Record<string, unknown>): Activit
since: validateTimestamp(search["since"]),
until: validateTimestamp(search["until"]),
domain: validateText(search["domain"]),
client: validateText(search["client"]),
client: validateClients(search["client"]),
blocked: validateBlocked(search["blocked"]),
};
}