From 9dc223c7be9fda11e672977ee224734d0177b92a Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:29:46 +0200 Subject: [PATCH 1/2] Untangle the nested ternaries in e2e, common and three socket hooks (GRYT-1792, client stack 3) Twelve files. Sort comparators write their last step as `a < b ? -1 : Number(a > b)`. Longer chains became early-return functions (filteredAction, lostSince) or two named steps. The send-across-restart spec opens its conversation from a table keyed by kind. useCustomThemes looks a preset up first and a saved theme second, and still doesn't let a missing preset fall through to a saved theme. Same output for every input as before. The twelve files leave the nested-ternary exception list. Co-Authored-By: Claude Opus 5.5 --- .eslintrc.cjs | 12 ------------ e2e/playwright.config.ts | 5 ++++- e2e/soak/support/probes.ts | 3 ++- e2e/soak/support/tracker.ts | 6 ++++-- e2e/tests/send-across-restart.spec.ts | 11 ++++++----- src/lib/reports/callTimeline.ts | 8 ++++++-- src/packages/addons/src/pluginPanels.ts | 2 +- .../src/components/NotificationLevelMenu.tsx | 4 +--- src/packages/common/src/hooks/useCustomThemes.ts | 10 +++------- src/packages/common/src/hooks/useTheme.ts | 3 ++- .../socket/src/hooks/contactFilterStore.ts | 14 +++++++++----- src/packages/socket/src/hooks/dmDirectory.ts | 2 +- src/packages/socket/src/hooks/mergeMessages.ts | 2 +- 13 files changed, 40 insertions(+), 42 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2aded6983..438f310f5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -92,16 +92,7 @@ module.exports = { // Nested ternaries from before the rule. { files: [ - "e2e/playwright.config.ts", - "e2e/soak/support/probes.ts", - "e2e/soak/support/tracker.ts", - "e2e/tests/send-across-restart.spec.ts", - "src/lib/reports/callTimeline.ts", - "src/packages/addons/src/pluginPanels.ts", "src/packages/common/src/auth/keycloak.ts", - "src/packages/common/src/components/NotificationLevelMenu.tsx", - "src/packages/common/src/hooks/useCustomThemes.ts", - "src/packages/common/src/hooks/useTheme.ts", "src/packages/socket/src/components/ChannelList.tsx", "src/packages/socket/src/components/ChatEditor.tsx", "src/packages/socket/src/components/ChatMessage.tsx", @@ -146,9 +137,6 @@ module.exports = { "src/packages/socket/src/components/memberInvite.ts", "src/packages/socket/src/components/serverView.tsx", "src/packages/socket/src/components/tileColor.ts", - "src/packages/socket/src/hooks/contactFilterStore.ts", - "src/packages/socket/src/hooks/dmDirectory.ts", - "src/packages/socket/src/hooks/mergeMessages.ts", "src/packages/socket/src/hooks/useEmoteImport.ts", "src/packages/socket/src/hooks/useHistoryWindow.ts", "src/packages/socket/src/hooks/useOpenDmSpace.ts", diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 9a6ede71f..d62e6c7a8 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -2,6 +2,9 @@ import { defineConfig, devices } from "@playwright/test"; const CI = !!process.env.CI; +// Each worker starts its own server, so a shared one gets one worker. +const WORKERS = CI ? 4 : 3; + export default defineConfig({ testDir: "./tests", outputDir: "./test-results", @@ -9,7 +12,7 @@ export default defineConfig({ forbidOnly: CI, retries: CI ? 1 : 0, // Each worker starts its own server. CI runners on this public repo have four cores, so four workers. - workers: process.env.GRYT_E2E_SERVER ? 1 : CI ? 4 : 3, + workers: process.env.GRYT_E2E_SERVER ? 1 : WORKERS, timeout: 60_000, expect: { timeout: 15_000 }, // CI shards the suite across several runners (see ci.yml); each writes its own blob file, diff --git a/e2e/soak/support/probes.ts b/e2e/soak/support/probes.ts index c32dc297b..f086ac9a8 100644 --- a/e2e/soak/support/probes.ts +++ b/e2e/soak/support/probes.ts @@ -151,7 +151,8 @@ export function networkWatch(log: EventWriter, everyMs = 5000): Stoppable { for (const [name, list] of Object.entries(networkInterfaces())) { for (const a of list ?? []) { if (a.internal) continue; - const scope = a.family === "IPv6" ? (a.address.startsWith("fe80") ? "link" : "global") : "v4"; + const v6scope = a.address.startsWith("fe80") ? "link" : "global"; + const scope = a.family === "IPv6" ? v6scope : "v4"; const id = createHash("sha256").update(a.address).digest("hex").slice(0, 8); seen.set(`${name}/${scope}/${id}`, a.family); } diff --git a/e2e/soak/support/tracker.ts b/e2e/soak/support/tracker.ts index 04f9e438c..bae8ecb5f 100644 --- a/e2e/soak/support/tracker.ts +++ b/e2e/soak/support/tracker.ts @@ -78,10 +78,12 @@ export class Tracker { status(): string { const servers = [...this.serverUp.values()]; - const sio = servers.length === 0 ? "sio -" : servers.every(Boolean) ? "sio up" : "sio DOWN"; + const allUp = servers.every(Boolean) ? "sio up" : "sio DOWN"; + const sio = servers.length === 0 ? "sio -" : allUp; const ice = [...this.ice.values()].filter((s) => s !== "closed"); const media = ice.length === 0 ? "" : ` ice ${ice.join("/")} rtt ${this.last.rtt ?? "?"}ms aob ${this.last.aob ? Math.round(this.last.aob / 1000) + "k" : "?"} audio ${this.last.audioFlowing ?? 0}/${this.last.audioIn ?? 0}`; - const voice = this.inVoice === null ? "" : this.inVoice ? " voice in" : " voice OUT"; + const inOut = this.inVoice ? " voice in" : " voice OUT"; + const voice = this.inVoice === null ? "" : inOut; const { serverDrops, sfuDrops, iceDrops } = this.counts; return `${this.who.padEnd(14)} ${sio} sfu-ws ${this.sfuOpen}${voice}${media} drops sio ${serverDrops} sfu ${sfuDrops} ice ${iceDrops}`; } diff --git a/e2e/tests/send-across-restart.spec.ts b/e2e/tests/send-across-restart.spec.ts index e8814f326..8bf099e8a 100644 --- a/e2e/tests/send-across-restart.spec.ts +++ b/e2e/tests/send-across-restart.spec.ts @@ -182,9 +182,10 @@ async function rowStates(rows: Locator, nonces: Set): Promise const seen = await rows.evaluateAll((els) => els.map((el) => ({ id: el.getAttribute("data-message-id") ?? "", failed: !!el.textContent?.includes("Failed to send") })), ); - return seen.map(({ id, failed }) => - failed ? "failed" : id.startsWith("pending-") || nonces.has(id) ? "pending" : "sent", - ); + return seen.map(({ id, failed }) => { + if (failed) return "failed"; + return id.startsWith("pending-") || nonces.has(id) ? "pending" : "sent"; + }); } for (const kind of ["channel", "dm", "thread"] as const) { @@ -207,8 +208,8 @@ for (const kind of ["channel", "dm", "thread"] as const) { await joinServer(bob.page, server.host); } - const conversation = - kind === "channel" ? channel(alice.page) : kind === "dm" ? await dm(alice, bob!) : await thread(alice.page); + const opener = { channel: () => channel(alice.page), dm: () => dm(alice, bob!), thread: () => thread(alice.page) }; + const conversation = await opener[kind](); // Here before the restart, so a first page rebuilt after it has to carry it. const before = unique("said before the restart"); diff --git a/src/lib/reports/callTimeline.ts b/src/lib/reports/callTimeline.ts index 3ae8058bb..f4f3231f0 100644 --- a/src/lib/reports/callTimeline.ts +++ b/src/lib/reports/callTimeline.ts @@ -26,9 +26,13 @@ let before: CallStatsSample | null = null; const ms = (v: number | null) => (v === null ? "?" : `${Math.round(v)}ms`); /** One line: what the call looked like this second, and what changed since the last. */ +function lostSince(now: number | null, before: number | null | undefined): string { + if (now === null) return "?"; + return before == null ? String(now) : `+${now - before}`; +} + export function formatCallSample(secs: number, now: CallStatsSample, prev: CallStatsSample | null): string { - const lost = - now.packetsLost === null ? "?" : prev?.packetsLost == null ? String(now.packetsLost) : `+${now.packetsLost - prev.packetsLost}`; + const lost = lostSince(now.packetsLost, prev?.packetsLost); return [ `+${secs}s`, `socket ${ms(now.socketRttMs)}`, diff --git a/src/packages/addons/src/pluginPanels.ts b/src/packages/addons/src/pluginPanels.ts index 762f86b9e..7011fea0b 100644 --- a/src/packages/addons/src/pluginPanels.ts +++ b/src/packages/addons/src/pluginPanels.ts @@ -27,7 +27,7 @@ function republish(): void { * By addon id, compared with `<` rather than `localeCompare`: under `nb` Chrome * answers -1 for `"zzz".localeCompare("aaa")`, so the order was not sorted. */ - snapshot = [...panels.values()].sort((a, b) => (a.addonId < b.addonId ? -1 : a.addonId > b.addonId ? 1 : 0)); + snapshot = [...panels.values()].sort((a, b) => (a.addonId < b.addonId ? -1 : Number(a.addonId > b.addonId))); for (const listener of listeners) listener(); } diff --git a/src/packages/common/src/components/NotificationLevelMenu.tsx b/src/packages/common/src/components/NotificationLevelMenu.tsx index cfa828218..48846bc87 100644 --- a/src/packages/common/src/components/NotificationLevelMenu.tsx +++ b/src/packages/common/src/components/NotificationLevelMenu.tsx @@ -75,9 +75,7 @@ export function NotificationLevelMenu({ parentItemId: placement?.parentItemId ?? null, defaultLevel: getPlacement(host, scope.id)?.defaultLevel ?? null, }) - : scope.kind === "folder" - ? (getOwnLevel(host, { kind: "server" }) ?? "all") - : "all"; + : (scope.kind === "folder" && getOwnLevel(host, { kind: "server" })) || "all"; /* What this scope currently comes out as: its own setting, else what it inherits. */ const resolved: NotificationLevel = own ?? inherited; diff --git a/src/packages/common/src/hooks/useCustomThemes.ts b/src/packages/common/src/hooks/useCustomThemes.ts index c326cd3df..3a70e5f29 100644 --- a/src/packages/common/src/hooks/useCustomThemes.ts +++ b/src/packages/common/src/hooks/useCustomThemes.ts @@ -156,13 +156,9 @@ function useCustomThemesImpl(): CustomThemesState { * A preset the library ships, or one somebody saved. Presets are stored as * "preset:", so a newer @gryt/ui brings its corrections with it. */ - const activeTheme = - activeId === null - ? null - : activeId.startsWith(PRESET_PREFIX) - ? (grytPresetsById.get(activeId.slice(PRESET_PREFIX.length))?.theme ?? - null) - : (themes.find((entry) => entry.id === activeId)?.theme ?? null); + const preset = activeId?.startsWith(PRESET_PREFIX) ? grytPresetsById.get(activeId.slice(PRESET_PREFIX.length)) : null; + const saved = activeId === null || preset !== null ? null : themes.find((entry) => entry.id === activeId); + const activeTheme = (preset ?? saved)?.theme ?? null; return { themes, diff --git a/src/packages/common/src/hooks/useTheme.ts b/src/packages/common/src/hooks/useTheme.ts index 3a9cfe50f..e7185e4fd 100644 --- a/src/packages/common/src/hooks/useTheme.ts +++ b/src/packages/common/src/hooks/useTheme.ts @@ -119,8 +119,9 @@ function useThemeHook(): ThemeSettings { return () => systemPrefersDark.removeEventListener("change", handleChange); }, [systemPrefersDark]); + const systemAppearance = systemIsDark ? "dark" : "light"; const resolvedAppearance: AppearanceResolved = - appearancePreference === "system" ? (systemIsDark ? "dark" : "light") : appearancePreference; + appearancePreference === "system" ? systemAppearance : appearancePreference; function setAccentColor(value: AccentColor) { setAccentColorState(value); diff --git a/src/packages/socket/src/hooks/contactFilterStore.ts b/src/packages/socket/src/hooks/contactFilterStore.ts index 072ae3206..49dc4a65a 100644 --- a/src/packages/socket/src/hooks/contactFilterStore.ts +++ b/src/packages/socket/src/hooks/contactFilterStore.ts @@ -94,13 +94,16 @@ function commit(next: FilteredItem[]): void { /** What they did, in a few words. */ export function filteredSummary(item: FilteredItem): string { const times = item.count === 1 ? "" : ` ${item.count} times`; - const what = - item.kind === "call" ? `Called you${times}` - : item.kind === "conversation" ? "Started a conversation" - : item.count === 1 ? "Sent a message" : `Sent ${item.count} messages`; + const what = filteredAction(item, times); return item.reason === "flood" ? `${what}, in a burst` : what; } +function filteredAction(item: FilteredItem, times: string): string { + if (item.kind === "call") return `Called you${times}`; + if (item.kind === "conversation") return "Started a conversation"; + return item.count === 1 ? "Sent a message" : `Sent ${item.count} messages`; +} + /** A conversation and its messages are one row, and a call is its own. */ function rowKind(kind: FilteredEvent["kind"]): "call" | "message" { return kind === "call" ? "call" : "message"; @@ -113,7 +116,8 @@ export function recordFiltered(event: FilteredEvent): void { // The conversation arriving adds no message to the count; its first message does. const opened = event.kind === "conversation"; const kind = opened ? (prev?.kind ?? "conversation") : event.kind; - const count = opened ? (prev?.count ?? 1) : prev?.kind === "conversation" ? 1 : (prev?.count ?? 0) + 1; + const nextCount = prev?.kind === "conversation" ? 1 : (prev?.count ?? 0) + 1; + const count = opened ? (prev?.count ?? 1) : nextCount; const item: FilteredItem = { host: event.host, conversationId: event.conversationId, diff --git a/src/packages/socket/src/hooks/dmDirectory.ts b/src/packages/socket/src/hooks/dmDirectory.ts index 78ad2c810..416d18b5a 100644 --- a/src/packages/socket/src/hooks/dmDirectory.ts +++ b/src/packages/socket/src/hooks/dmDirectory.ts @@ -43,7 +43,7 @@ function newestFirst(a: DirectoryEntry, b: DirectoryEntry): number { } function compare(a: string, b: string): number { - return a < b ? -1 : a > b ? 1 : 0; + return a < b ? -1 : Number(a > b); } /** diff --git a/src/packages/socket/src/hooks/mergeMessages.ts b/src/packages/socket/src/hooks/mergeMessages.ts index 42df2c380..53bf05cce 100644 --- a/src/packages/socket/src/hooks/mergeMessages.ts +++ b/src/packages/socket/src/hooks/mergeMessages.ts @@ -15,7 +15,7 @@ function compareMessages(a: ChatMessage, b: ChatMessage): number { if (!!a.pending !== !!b.pending) return a.pending ? 1 : -1; const diff = millis(a.created_at) - millis(b.created_at); if (diff !== 0) return diff; - return a.message_id < b.message_id ? -1 : a.message_id > b.message_id ? 1 : 0; + return a.message_id < b.message_id ? -1 : Number(a.message_id > b.message_id); } /** From 7fe3c251cfbecbef15d2a75fb68395cfce5040c8 Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:34:11 +0200 Subject: [PATCH 2/2] Keep formatCallSample's doc comment on formatCallSample Co-Authored-By: Claude Opus 5.5 --- src/lib/reports/callTimeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/reports/callTimeline.ts b/src/lib/reports/callTimeline.ts index f4f3231f0..21a34234c 100644 --- a/src/lib/reports/callTimeline.ts +++ b/src/lib/reports/callTimeline.ts @@ -25,12 +25,12 @@ let before: CallStatsSample | null = null; const ms = (v: number | null) => (v === null ? "?" : `${Math.round(v)}ms`); -/** One line: what the call looked like this second, and what changed since the last. */ function lostSince(now: number | null, before: number | null | undefined): string { if (now === null) return "?"; return before == null ? String(now) : `+${now - before}`; } +/** One line: what the call looked like this second, and what changed since the last. */ export function formatCallSample(secs: number, now: CallStatsSample, prev: CallStatsSample | null): string { const lost = lostSince(now.packetsLost, prev?.packetsLost); return [