From 1529f2f64d4e644b80dd643c509e2f5a7cb12aeb Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:24:11 +0200 Subject: [PATCH 1/2] Untangle the nested ternaries in electron/ and src/components (GRYT-1792, client stack 1) Twelve files. Each chain became a lookup table, an early-return function (ringStateOf, iconOpacity, splashMessage, volumeColour, flag), or separate conditions where JSX picked between pages. The status banner's message and glyph now come from one table, so the two can't disagree. check-embedded-starting builds the sidebar's ring state from source, so it picks up ringStateOf() as well. 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 | 13 ------- electron/embeddedServerManager.ts | 7 ++-- electron/gameList.ts | 2 +- electron/helperStartup.ts | 3 +- electron/presenceHelper.ts | 5 ++- electron/processWatcher.ts | 9 +++-- scripts/check-embedded-starting.mjs | 5 ++- src/components/AuthLoadingOverlay.tsx | 15 ++++---- src/components/mainApp.tsx | 17 ++++----- src/components/microphoneDebugOverlay.tsx | 10 +++-- src/components/onboarding/OnboardingTour.tsx | 6 +-- src/components/serviceStatusBanner.tsx | 21 +++++----- src/components/sidebar.tsx | 40 ++++++++++---------- src/components/updateToastState.ts | 6 ++- src/components/videoDebugOverlay.tsx | 13 +++++-- 15 files changed, 87 insertions(+), 85 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d15b2cb61..068f1fb70 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -96,19 +96,6 @@ module.exports = { "e2e/soak/support/probes.ts", "e2e/soak/support/tracker.ts", "e2e/tests/send-across-restart.spec.ts", - "electron/embeddedServerManager.ts", - "electron/gameList.ts", - "electron/helperStartup.ts", - "electron/presenceHelper.ts", - "electron/processWatcher.ts", - "src/components/AuthLoadingOverlay.tsx", - "src/components/mainApp.tsx", - "src/components/microphoneDebugOverlay.tsx", - "src/components/onboarding/OnboardingTour.tsx", - "src/components/serviceStatusBanner.tsx", - "src/components/sidebar.tsx", - "src/components/updateToastState.ts", - "src/components/videoDebugOverlay.tsx", "src/lib/reports/callTimeline.ts", "src/packages/addons/src/pluginPanels.ts", "src/packages/common/src/auth/keycloak.ts", diff --git a/electron/embeddedServerManager.ts b/electron/embeddedServerManager.ts index 264189278..5dea0db4b 100644 --- a/electron/embeddedServerManager.ts +++ b/electron/embeddedServerManager.ts @@ -95,8 +95,7 @@ function explainExit( source: LogSource, code: number | null, ): string { - const label = - source === "sfu" ? "SFU" : source === "worker" ? "Media worker" : "Server"; + const label = { sfu: "SFU", worker: "Media worker" }[source as string] ?? "Server"; const lines = recentOutput.get(outputKey(owner, source)) ?? []; // Prefer a line that names the actual failure over the last line, which is @@ -347,7 +346,7 @@ function readBundledVersions(): { server?: string; sfu?: string; worker?: string const runtimeRoot = packagedRuntimeRoot(); const packaged = runtimeRoot ? join(runtimeRoot, "versions.json") : null; const dev = join(app.getAppPath(), "build", "embedded-server", "versions.json"); - const path = packaged && existsSync(packaged) ? packaged : existsSync(dev) ? dev : null; + const path = [packaged, dev].find((p) => p && existsSync(p)) ?? null; if (!path) return {}; try { @@ -374,7 +373,7 @@ function getWorkerEntryPath(): string | null { function getSfuBinaryPath(): string | null { const ext = process.platform === "win32" ? ".exe" : ""; const name = `gryt_sfu${ext}`; - const ebOs = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux"; + const ebOs = { win32: "win", darwin: "mac" }[process.platform as string] ?? "linux"; const ebArch = process.arch === "arm64" ? "arm64" : "x64"; const runtimeRoot = packagedRuntimeRoot(); const packaged = runtimeRoot ? join(runtimeRoot, "sfu", name) : null; diff --git a/electron/gameList.ts b/electron/gameList.ts index 68325493c..47a132731 100644 --- a/electron/gameList.ts +++ b/electron/gameList.ts @@ -72,7 +72,7 @@ export function createGameIndex(games: readonly GameEntry[], platform: NodeJS.Pl const byComm = new Map(); // Proton runs Windows games on Linux under their Windows names. - const oses: GameOs[] = platform === "linux" ? ["linux", "win32"] : platform === "darwin" ? ["darwin"] : ["win32"]; + const oses: GameOs[] = ({ linux: ["linux", "win32"], darwin: ["darwin"] } as Record)[platform] ?? ["win32"]; for (const game of games) { for (const id of game.ids) if (!byApp.has(id)) byApp.set(id, game.name); diff --git a/electron/helperStartup.ts b/electron/helperStartup.ts index 5768e0bad..81cbceabe 100644 --- a/electron/helperStartup.ts +++ b/electron/helperStartup.ts @@ -193,7 +193,8 @@ export const runQuietly = (command: string, args: string[]): Promise => new Promise((resolve) => { execFile(command, args, { timeout: 10_000, windowsHide: true }, (err) => { const code = (err as { code?: unknown } | null)?.code; - resolve(err ? (typeof code === "number" ? code : 1) : 0); + if (!err) resolve(0); + else resolve(typeof code === "number" ? code : 1); }); }); diff --git a/electron/presenceHelper.ts b/electron/presenceHelper.ts index e020a712a..fadfdf220 100644 --- a/electron/presenceHelper.ts +++ b/electron/presenceHelper.ts @@ -134,6 +134,9 @@ export function createPresenceSource(options: PresenceSourceOptions): PresenceSo link = null; await this.stop(); }, - via: () => (link ? "helper" : host ? "app" : null), + via: () => { + if (link) return "helper"; + return host ? "app" : null; + }, }; } diff --git a/electron/processWatcher.ts b/electron/processWatcher.ts index 4d7c8f9a9..e2554cfd0 100644 --- a/electron/processWatcher.ts +++ b/electron/processWatcher.ts @@ -231,9 +231,12 @@ export async function listRunningPrograms(): Promise { seen.set(key, basename(raw.replace(/\\/g, "/"))); } - return [...seen.values()].sort((a, b) => - a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0, - ); + return [...seen.values()].sort((a, b) => { + const x = a.toLowerCase(); + const y = b.toLowerCase(); + if (x === y) return 0; + return x < y ? -1 : 1; + }); } /* ── The watcher ─────────────────────────────────────────────────────── */ diff --git a/scripts/check-embedded-starting.mjs b/scripts/check-embedded-starting.mjs index 61832997b..497a9b9d1 100644 --- a/scripts/check-embedded-starting.mjs +++ b/scripts/check-embedded-starting.mjs @@ -61,7 +61,10 @@ const statusLines = (() => { assert.notEqual(start, -1, `${SIDEBAR} no longer reads rawStatus. Move this check with it.`); const ring = sidebar.indexOf("const ringState", start); assert.notEqual(ring, -1, `${SIDEBAR} no longer declares ringState after rawStatus. Move this check with it.`); - return sidebar.slice(start, sidebar.indexOf(";", ring) + 1).replace(/: ServerRingState/g, ""); + const fn = sidebar.indexOf("function ringStateOf("); + assert.notEqual(fn, -1, `${SIDEBAR} no longer has ringStateOf(). Move this check with it.`); + const ringStateOf = sidebar.slice(fn, sidebar.indexOf("\n}\n", fn) + 2).replace(/\(s: \{[^}]*\}\): ServerRingState/, "(s)"); + return `${ringStateOf}\n${sidebar.slice(start, sidebar.indexOf(";", ring) + 1)}`; })(); /* The hover card's connection words, each with the condition it is drawn under. A new one diff --git a/src/components/AuthLoadingOverlay.tsx b/src/components/AuthLoadingOverlay.tsx index 617fa9707..c0c421284 100644 --- a/src/components/AuthLoadingOverlay.tsx +++ b/src/components/AuthLoadingOverlay.tsx @@ -29,6 +29,12 @@ const SPLASH_COPY = { slow: "This is taking longer than it should. Gryt will carry on without an account in a moment.", } as const; +function splashMessage(reach: AccountsReach | null, slow: boolean) { + if (reach === "unreachable") return "accounts-down"; + if (reach === "offline") return "offline"; + return slow ? "slow" : "normal"; +} + export function AuthLoadingOverlay({ open, fadeDurationMs = 450, @@ -62,14 +68,7 @@ export function AuthLoadingOverlay({ }; }, [open]); - const message = - reach === "unreachable" - ? "accounts-down" - : reach === "offline" - ? "offline" - : slow - ? "slow" - : "normal"; + const message = splashMessage(reach, slow); useEffect(() => { if (!open) { diff --git a/src/components/mainApp.tsx b/src/components/mainApp.tsx index 1c97a54cb..35e1a2e03 100644 --- a/src/components/mainApp.tsx +++ b/src/components/mainApp.tsx @@ -61,9 +61,9 @@ export function MainApp() { /* What a bug report calls "where you were". Recorded here rather than in the report form, which would always answer "the report form". */ - useRememberPlace( - showDiscovery ? "discovery" : Object.keys(servers).length > 0 ? "server" : "empty", - ); + const hasServers = Object.keys(servers).length > 0; + const emptyPlace = hasServers ? "server" : "empty"; + useRememberPlace(showDiscovery ? "discovery" : emptyPlace); /* The page to reopen on the next launch. Not the one above, which is only what a bug report says and is never read back. */ @@ -87,13 +87,10 @@ export function MainApp() { - {dmSpaceOpen && Object.keys(servers).length > 0 ? ( - - ) : showDiscovery ? ( - - ) : Object.keys(servers).length > 0 ? ( - - ) : ( + {dmSpaceOpen && hasServers && } + {!(dmSpaceOpen && hasServers) && showDiscovery && } + {!dmSpaceOpen && !showDiscovery && hasServers && } + {!showDiscovery && !hasServers && ( /* Was a line of text with a finger emoji pointing at the sidebar. That is the tour's job; somebody who dismissed it still ends up here. */
diff --git a/src/components/microphoneDebugOverlay.tsx b/src/components/microphoneDebugOverlay.tsx index 3fcf0ba80..0aa02b48d 100644 --- a/src/components/microphoneDebugOverlay.tsx +++ b/src/components/microphoneDebugOverlay.tsx @@ -16,6 +16,11 @@ const StateDot = ({ on }: { on: boolean }) => ( ); +function volumeColour(volume: number): string { + if (volume > 80) return "var(--gryt-danger-9)"; + return volume > 50 ? "var(--gryt-warning-9)" : "var(--gryt-success-9)"; +} + export function MicrophoneDebugOverlay({ isVisible }: MicrophoneDebugOverlayProps) { const { microphoneBuffer, @@ -143,7 +148,7 @@ export function MicrophoneDebugOverlay({ isVisible }: MicrophoneDebugOverlayProp
ID: {micID ? micID.slice(0, 8) + "..." : "None"}
Name: {currentDevice?.label || "Unknown"}
-
Available: {devicesLoading ? "Loading..." : devicesError ? "Error" : `${devices.length} devices`}
+
Available: {devicesLoading && "Loading..."}{!devicesLoading && devicesError && "Error"}{!devicesLoading && !devicesError && `${devices.length} devices`}
{devicesError && (
{devicesError} @@ -201,8 +206,7 @@ export function MicrophoneDebugOverlay({ isVisible }: MicrophoneDebugOverlayProp
80 ? "var(--gryt-danger-9)" : - processedVolume > 50 ? "var(--gryt-warning-9)" : "var(--gryt-success-9)", + backgroundColor: volumeColour(processedVolume), transition: "width 0.1s ease-out" }} />
diff --git a/src/components/onboarding/OnboardingTour.tsx b/src/components/onboarding/OnboardingTour.tsx index ac1db7eb6..12ade9fa2 100644 --- a/src/components/onboarding/OnboardingTour.tsx +++ b/src/components/onboarding/OnboardingTour.tsx @@ -324,9 +324,7 @@ export function OnboardingTour({ onFinish }: { onFinish: () => void }) { const rightOfTarget = cut ? cut.left + cut.width + OFFSET : 0; const fitsOnTheRight = rightOfTarget + CARD_WIDTH + 16 <= window.innerWidth; - const card = !cut - ? null - : step.side === "right" + const card = cut && (step.side === "right" ? { top: Math.min( Math.max(cut.top + cut.height / 2 - 90, 16), @@ -342,7 +340,7 @@ export function OnboardingTour({ onFinish }: { onFinish: () => void }) { Math.max(cut.left + cut.width / 2 - CARD_WIDTH / 2, 16), window.innerWidth - CARD_WIDTH - 16 ) - }; + }); // **Portaled to body, and it has to be.** The app renders inside // `.radix-themes`, a stacking context, so no z-index would beat a dialog. diff --git a/src/components/serviceStatusBanner.tsx b/src/components/serviceStatusBanner.tsx index 195a737e0..6a78c2137 100644 --- a/src/components/serviceStatusBanner.tsx +++ b/src/components/serviceStatusBanner.tsx @@ -54,6 +54,12 @@ const TONE: Record = { const OFFLINE = "You're offline, so Gryt couldn't check your account. It'll check again once you're back online."; +/* Not reachable is a warning whatever else is going on. Offline is just a fact. */ +const UNANNOUNCED = { + unreachable: { message: GENERIC, Glyph: PiWarningFill }, + offline: { message: OFFLINE, Glyph: PiInfoFill }, +}; + /** * Signed in, or launched while accounts couldn't be checked. A guest on a normal * day has nothing that breaks when the account services go down. @@ -144,21 +150,12 @@ export function ServiceStatusBanner() { if (!banner) return null; - const message = + const { message, Glyph } = banner.kind === "announced" - ? banner.announcement.message - : banner.kind === "offline" - ? OFFLINE - : GENERIC; + ? { message: banner.announcement.message, Glyph: ICON[banner.announcement.type] } + : UNANNOUNCED[banner.kind]; const tone = banner.kind === "announced" ? TONE[banner.announcement.type] : "warning"; - /* Not reachable is a warning whatever else is going on. Offline is just a fact. */ - const Glyph = - banner.kind === "announced" - ? ICON[banner.announcement.type] - : banner.kind === "offline" - ? PiInfoFill - : PiWarningFill; if (message === dismissed) return null; diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 1643f8297..1747a7590 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -68,6 +68,20 @@ async function copyServerAddress(host: string): Promise { } } +function ringStateOf(s: { isStarting: boolean; isSettling: boolean; isReconnecting: boolean; isConnecting: boolean }): ServerRingState { + if (s.isStarting) return "starting"; + if (s.isSettling) return "settling"; + if (s.isReconnecting) return "reconnecting"; + return s.isConnecting ? "connecting" : "none"; +} + +function iconOpacity(s: { bright: boolean; isUnavailable: boolean; awaitingApproval: boolean; isReconnecting: boolean }) { + if (s.bright) return 1; + if (s.isUnavailable) return 0.45; + if (s.awaitingApproval) return 0.4; + return s.isReconnecting ? undefined : 0.5; +} + export function Sidebar({ setShowAddServer }: SidebarProps) { const { isSignedIn, login, logout } = useAccount(); const { nickname, avatarDataUrl, setShowSettings, openSettings } = useSettings(); @@ -439,15 +453,7 @@ function ServerItem({ */ const isSettling = !rawStatus && !settleExpired && !isStarting; - const ringState: ServerRingState = isStarting - ? "starting" - : isSettling - ? "settling" - : isReconnecting - ? "reconnecting" - : isConnecting - ? "connecting" - : "none"; + const ringState = ringStateOf({ isStarting, isSettling, isReconnecting, isConnecting }); /* Waiting on a moderator (GRYT-289). Not a connection state: the server is reachable and answering, it just has not let this person in yet. */ const awaitingApproval = Boolean(servers[host]?.approvalRequestedAt); @@ -490,16 +496,12 @@ function ServerItem({ style={{ // A server you just created keeps its colour. Greying it // would say something is wrong; the ring says not ready. - opacity: - currentlyViewingServer?.host === host || isStarting - ? 1 - : isUnavailable - ? 0.45 - : awaitingApproval - ? 0.4 - : isReconnecting - ? undefined - : 0.5, + opacity: iconOpacity({ + bright: currentlyViewingServer?.host === host || isStarting, + isUnavailable, + awaitingApproval, + isReconnecting, + }), filter: !isStarting && (isUnavailable || isReconnecting || awaitingApproval) diff --git a/src/components/updateToastState.ts b/src/components/updateToastState.ts index 73b3fd010..bff48fb69 100644 --- a/src/components/updateToastState.ts +++ b/src/components/updateToastState.ts @@ -1,6 +1,10 @@ import type { UpdateStatus } from "../lib/electron"; /** One id for every release, so a newer one redraws the toast in place rather than stacking a second. */ +function downloadPhase(autoDownload: boolean | undefined) { + return autoDownload ? "downloading" : "waiting"; +} + export const UPDATE_TOAST_ID = "update"; /** What the toast is showing about the release it named. */ @@ -38,7 +42,7 @@ export function nextShown(current: Shown | null, status: UpdateStatus): Shown | version: status.version, /* Announced with `autoDownload` means the bytes are already moving — the main process says so once electron-updater starts fetching. */ - phase: status.storeManaged ? "store" : status.autoDownload ? "downloading" : "waiting", + phase: status.storeManaged ? "store" : downloadPhase(status.autoDownload), /* Only when the release changed: a press during a download installs that same one. */ newerAfterPress: Boolean(status.installWhenReady) && diff --git a/src/components/videoDebugOverlay.tsx b/src/components/videoDebugOverlay.tsx index 290ec5b70..4a3735ead 100644 --- a/src/components/videoDebugOverlay.tsx +++ b/src/components/videoDebugOverlay.tsx @@ -194,11 +194,16 @@ function useVideoDebugDiagnostics(enabled: boolean): VideoDiagnostics { return diagnostics; } +function flag(value: boolean | null | undefined, yes: string, no: string): string | null { + if (value == null) return null; + return value ? yes : no; +} + function SenderSection({ s }: { s: SenderDiagnostics }) { const stateParts = [ s.readyState ?? "—", - s.enabled == null ? null : s.enabled ? "enabled" : "disabled", - s.muted == null ? null : s.muted ? "muted" : "unmuted", + flag(s.enabled, "enabled", "disabled"), + flag(s.muted, "muted", "unmuted"), ].filter(Boolean); return ( @@ -211,7 +216,7 @@ function SenderSection({ s }: { s: SenderDiagnostics }) { {s.frameRate != null ? ` @ ${fmt(s.frameRate, 1)} fps` : ""}
- Encoding: {s.encodingActive == null ? "—" : s.encodingActive ? "active" : "inactive"} + Encoding: {flag(s.encodingActive, "active", "inactive") ?? "—"} {" / "}max {s.maxBitrateKbps != null ? `${fmtInt(s.maxBitrateKbps)} kbps` : "—"}
@@ -376,7 +381,7 @@ export function VideoDebugOverlay({ isVisible }: VideoDebugOverlayProps) { icon={} status={{ active: isConnected && hasVideo, - label: isConnected ? (hasVideo ? "Streaming" : "No video") : "Disconnected", + label: isConnected ? flag(hasVideo, "Streaming", "No video") ?? "" : "Disconnected", }} initialPosition={{ x: window.innerWidth - 680, y: 10 }} > From c75dcef722b7542aad13c8c3aa4ea4f600596be2 Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:33:20 +0200 Subject: [PATCH 2/2] Keep UPDATE_TOAST_ID's doc comment on UPDATE_TOAST_ID Co-Authored-By: Claude Opus 5.5 --- src/components/updateToastState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/updateToastState.ts b/src/components/updateToastState.ts index bff48fb69..bb502e5c3 100644 --- a/src/components/updateToastState.ts +++ b/src/components/updateToastState.ts @@ -1,10 +1,10 @@ import type { UpdateStatus } from "../lib/electron"; -/** One id for every release, so a newer one redraws the toast in place rather than stacking a second. */ function downloadPhase(autoDownload: boolean | undefined) { return autoDownload ? "downloading" : "waiting"; } +/** One id for every release, so a newer one redraws the toast in place rather than stacking a second. */ export const UPDATE_TOAST_ID = "update"; /** What the toast is showing about the release it named. */