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..bb502e5c3 100644 --- a/src/components/updateToastState.ts +++ b/src/components/updateToastState.ts @@ -1,5 +1,9 @@ import type { UpdateStatus } from "../lib/electron"; +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"; @@ -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 }} >