From a8a0b7db1b814129b0fa988d90ec1a1eedc97144 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 2 Jun 2026 07:44:24 -0700 Subject: [PATCH] infra: add linting and formatting for js projects (#230) * wip * wip * wip * format * wip(mobile): lint and format * cleanup * nits * nits * idiomatic react * nit * format all root files --- js/desktop/.eslintrc.json | 18 +- js/desktop/.prettierrc | 8 + js/desktop/forge.config.ts | 3 +- js/desktop/package.json | 8 +- js/desktop/src/App.tsx | 67 +- js/desktop/src/api/client.ts | 108 +- js/desktop/src/api/types.ts | 104 +- js/desktop/src/assets.d.ts | 4 +- .../src/autoplay_window/AutoplayApp.tsx | 6 +- js/desktop/src/autoplay_window/renderer.tsx | 4 +- .../src/components/app-error-boundary.tsx | 16 +- .../src/components/audio/audio-level-bars.tsx | 5 +- .../src/components/audio/use-audio-source.ts | 66 +- .../src/components/autoplay-card-content.tsx | 16 +- .../src/components/composing-indicator.tsx | 10 +- .../confirm-destructive-overlay.tsx | 24 +- js/desktop/src/components/copyable-email.tsx | 14 +- js/desktop/src/components/error-fallback.tsx | 32 +- .../src/components/in-app-autoplay-card.tsx | 12 +- .../src/components/keybindings-overlay.tsx | 23 +- .../src/components/link-preview-card.tsx | 15 +- .../src/components/relative-timestamp.tsx | 4 +- .../src/components/screen-source-picker.tsx | 25 +- js/desktop/src/components/ui/avatar.tsx | 63 +- js/desktop/src/components/ui/badge.tsx | 42 +- js/desktop/src/components/ui/breadcrumb.tsx | 65 +- js/desktop/src/components/ui/button.tsx | 61 +- js/desktop/src/components/ui/card.tsx | 63 +- js/desktop/src/components/ui/checkbox.tsx | 19 +- js/desktop/src/components/ui/context-menu.tsx | 94 +- js/desktop/src/components/ui/dialog.tsx | 73 +- .../src/components/ui/dropdown-menu.tsx | 94 +- js/desktop/src/components/ui/input.tsx | 14 +- js/desktop/src/components/ui/label.tsx | 14 +- js/desktop/src/components/ui/progress.tsx | 16 +- js/desktop/src/components/ui/radio-group.tsx | 18 +- js/desktop/src/components/ui/scroll-area.tsx | 20 +- js/desktop/src/components/ui/select.tsx | 79 +- js/desktop/src/components/ui/separator.tsx | 16 +- js/desktop/src/components/ui/skeleton.tsx | 10 +- js/desktop/src/components/ui/slider.tsx | 18 +- js/desktop/src/components/ui/sonner.tsx | 46 +- js/desktop/src/components/ui/switch.tsx | 18 +- js/desktop/src/components/ui/tabs.tsx | 50 +- js/desktop/src/components/ui/toggle-group.tsx | 50 +- js/desktop/src/components/ui/toggle.tsx | 34 +- js/desktop/src/components/ui/tooltip.tsx | 20 +- js/desktop/src/components/ui/typography.tsx | 132 +- .../src/components/video-audio-toggle.tsx | 14 +- js/desktop/src/components/window-controls.tsx | 22 +- js/desktop/src/config/env.ts | 44 +- js/desktop/src/electron.d.ts | 8 +- .../attachments/attachment-lightbox.tsx | 119 +- js/desktop/src/features/auth/code-step.tsx | 23 +- js/desktop/src/features/auth/email-step.tsx | 30 +- js/desktop/src/features/auth/login-page.tsx | 23 +- .../src/features/compose/attachment-strip.tsx | 45 +- .../src/features/compose/compose-overlay.tsx | 480 +++--- .../compose/compose-quota-indicator.tsx | 31 +- .../compose/configure-stream-step.tsx | 44 +- .../src/features/compose/markdown-editor.css | 5 +- .../src/features/compose/markdown-editor.tsx | 18 +- .../features/compose/recording-overlay.tsx | 100 +- .../features/compose/text-compose-step.tsx | 4 +- .../src/features/compose/text-editor.tsx | 52 +- .../src/features/compose/use-recorder.ts | 30 +- .../features/compose/use-screen-recorder.ts | 26 +- js/desktop/src/features/layout.tsx | 75 +- js/desktop/src/features/network-billing.tsx | 92 +- js/desktop/src/features/network-root.tsx | 79 +- js/desktop/src/features/network-selector.tsx | 71 +- js/desktop/src/features/network-settings.tsx | 83 +- .../particles/delete-particle-overlay.tsx | 21 +- .../particles/deleted-particle-view.tsx | 14 +- .../particles/fallback-particle-view.tsx | 39 +- .../src/features/particles/folder-view.tsx | 8 +- .../particles/media-particle-view.tsx | 124 +- .../particles/particle-attachments.tsx | 47 +- .../features/particles/particle-list-view.tsx | 200 +-- .../features/particles/particle-preview.tsx | 75 +- .../particles/particle-view-resolver.tsx | 35 +- .../particles/playback-page-indicator.tsx | 32 +- .../src/features/particles/reaction-bar.tsx | 88 +- .../particles/rename-stream-overlay.tsx | 41 +- .../src/features/particles/stream-card.tsx | 298 ++-- .../particles/stream-context-menu.tsx | 20 +- .../particles/stream-members-overlay.tsx | 152 +- .../particles/stream-presence-context.tsx | 21 +- .../src/features/particles/stream-top-bar.tsx | 161 +- .../src/features/particles/stream-view.tsx | 327 ++-- .../features/particles/text-edit-overlay.tsx | 31 +- .../features/particles/text-particle-view.tsx | 87 +- .../particles/text-reaction-input.tsx | 46 +- .../features/particles/transcript-overlay.tsx | 80 +- js/desktop/src/features/settings-page.tsx | 68 +- .../settings/audio-video-settings-page.tsx | 51 +- js/desktop/src/firebase.ts | 6 +- js/desktop/src/hooks/use-billing.ts | 15 +- js/desktop/src/hooks/use-channel.ts | 49 +- js/desktop/src/hooks/use-create-particle.ts | 33 +- js/desktop/src/hooks/use-dock-badge.ts | 27 +- js/desktop/src/hooks/use-download-url.ts | 11 +- .../src/hooks/use-effective-device-id.ts | 2 +- js/desktop/src/hooks/use-file-input.ts | 28 +- js/desktop/src/hooks/use-link-metadata.ts | 13 +- js/desktop/src/hooks/use-media-devices.ts | 59 +- js/desktop/src/hooks/use-member-management.ts | 24 +- js/desktop/src/hooks/use-network-usage.ts | 17 +- js/desktop/src/hooks/use-networks.ts | 8 +- js/desktop/src/hooks/use-object-url.ts | 23 + .../src/hooks/use-particle-attachments.ts | 16 +- js/desktop/src/hooks/use-particle.ts | 126 +- js/desktop/src/hooks/use-playback-keys.ts | 36 +- .../src/hooks/use-prefetch-adjacent-media.ts | 18 +- .../src/hooks/use-presence-positions.ts | 13 +- js/desktop/src/hooks/use-recording-mode.ts | 13 +- .../src/hooks/use-stream-action-keys.ts | 45 +- js/desktop/src/hooks/use-stream-autoplay.ts | 103 +- .../src/hooks/use-stream-keyboard-nav.ts | 46 +- .../src/hooks/use-stream-navigation-keys.ts | 23 +- js/desktop/src/hooks/use-stream-particles.ts | 49 +- js/desktop/src/hooks/use-stream-playback.ts | 193 ++- js/desktop/src/hooks/use-suspend-playback.ts | 4 +- .../src/hooks/use-transcript-playback.ts | 6 +- js/desktop/src/huddle_window/HuddleApp.tsx | 55 +- js/desktop/src/huddle_window/renderer.tsx | 4 +- js/desktop/src/index.css | 2 +- js/desktop/src/lib/constants.ts | 6 +- js/desktop/src/lib/errors.ts | 39 +- js/desktop/src/lib/firestore-particles.ts | 131 +- js/desktop/src/lib/humans.ts | 8 +- js/desktop/src/lib/image-thumbnail.ts | 7 +- js/desktop/src/lib/keyboard.ts | 4 +- js/desktop/src/lib/particle-path.ts | 13 +- js/desktop/src/lib/platform/desktop-only.ts | 11 +- js/desktop/src/lib/platform/electron.ts | 11 +- js/desktop/src/lib/platform/index.ts | 10 +- js/desktop/src/lib/platform/index.web.ts | 10 +- js/desktop/src/lib/platform/types.ts | 14 +- js/desktop/src/lib/platform/web.ts | 59 +- js/desktop/src/lib/pusher-client.ts | 76 +- js/desktop/src/lib/pusher-provider.tsx | 39 +- js/desktop/src/lib/query-client.ts | 16 +- js/desktop/src/lib/random-name.ts | 68 +- js/desktop/src/lib/router-shell.tsx | 4 +- js/desktop/src/lib/router-shell.web.tsx | 4 +- js/desktop/src/lib/sentry.ts | 13 +- js/desktop/src/lib/sentry.web.ts | 13 +- js/desktop/src/lib/sound-effects/engine.ts | 16 +- .../sound-effects/sound-effects-provider.tsx | 76 +- js/desktop/src/lib/sound-effects/sounds.ts | 24 +- .../src/lib/sound-effects/use-sound-effect.ts | 6 +- js/desktop/src/lib/stream-visibility.ts | 14 +- js/desktop/src/lib/time-utils.ts | 2 +- js/desktop/src/lib/utils.ts | 10 +- js/desktop/src/main.ts | 68 +- js/desktop/src/main/ipc-utils.ts | 6 +- js/desktop/src/main/sentry.ts | 15 +- js/desktop/src/preload.ts | 48 +- js/desktop/src/renderer.tsx | 4 +- .../ScreenRecordControlApp.tsx | 6 +- .../src/screen_record_window/renderer.tsx | 4 +- js/desktop/src/stores/auth-store.ts | 51 +- .../src/stores/autoplay-payload-store.ts | 4 +- js/desktop/src/stores/autoplay-store.ts | 6 +- js/desktop/src/stores/compose-intent-store.ts | 4 +- js/desktop/src/stores/media-devices-store.ts | 10 +- js/desktop/src/stores/media-settings-store.ts | 8 +- js/desktop/src/stores/playback-pause-store.ts | 2 +- js/desktop/src/stores/session-store.ts | 4 +- js/desktop/src/stores/sound-effects-store.ts | 8 +- js/desktop/src/styles/globals.css | 6 +- js/desktop/src/web/renderer.tsx | 12 +- js/desktop/yarn.lock | 497 +++++-- js/mobile/.prettierignore | 6 + js/mobile/.prettierrc | 8 + js/mobile/app.config.ts | 59 +- js/mobile/babel.config.js | 6 +- js/mobile/eslint.config.js | 25 + js/mobile/index.ts | 8 +- js/mobile/metro.config.js | 6 +- js/mobile/package.json | 8 +- js/mobile/src/App.tsx | 26 +- js/mobile/src/api/client.ts | 79 +- js/mobile/src/api/types.ts | 104 +- js/mobile/src/components/Avatar.tsx | 24 +- js/mobile/src/components/BottomSheet.tsx | 40 +- .../src/components/ComposingIndicator.tsx | 26 +- js/mobile/src/components/FlowyLogo.tsx | 4 +- js/mobile/src/components/ListSeparator.tsx | 2 +- .../src/components/RelativeTimestamp.tsx | 8 +- js/mobile/src/config/env.ts | 42 +- js/mobile/src/features/auth/SignInScreen.tsx | 48 +- .../compose/AudioRecordingOverlay.tsx | 45 +- .../src/features/compose/ComposeDock.tsx | 151 +- .../src/features/compose/ReviewSheet.tsx | 202 +-- .../src/features/compose/TextComposeModal.tsx | 147 +- .../compose/VideoRecordingOverlay.tsx | 34 +- .../src/features/huddle/HuddleScreen.tsx | 82 +- .../src/features/huddle/use-open-huddle.ts | 16 +- js/mobile/src/features/networks/Drawer.tsx | 152 +- .../features/networks/NetworkListScreen.tsx | 40 +- .../src/features/settings/AccountScreen.tsx | 14 +- .../src/features/settings/SettingsScreen.tsx | 10 +- .../stream-view/DeletedParticleView.tsx | 16 +- .../stream-view/EditParticleSheet.tsx | 33 +- .../stream-view/FallbackParticleView.tsx | 26 +- .../stream-view/MediaParticleView.tsx | 72 +- .../stream-view/PlaybackPageIndicator.tsx | 19 +- .../features/stream-view/ReactionSheet.tsx | 381 ++--- .../features/stream-view/ReactionStack.tsx | 32 +- .../stream-view/RenameStreamSheet.tsx | 41 +- .../stream-view/StreamActionsSheet.tsx | 46 +- .../stream-view/StreamMembersSheet.tsx | 85 +- .../stream-view/StreamMetadataHeader.tsx | 21 +- .../features/stream-view/StreamTopActions.tsx | 47 +- .../src/features/stream-view/StreamView.tsx | 184 ++- .../features/stream-view/StreamViewScreen.tsx | 23 +- .../features/stream-view/TextParticleView.tsx | 133 +- .../stream-view/TranscriptOverlay.tsx | 69 +- .../stream-view/stream-presence-context.tsx | 34 +- .../features/stream-view/stream-safe-area.tsx | 2 +- .../stream-view/use-exit-countdown.ts | 25 +- .../src/features/streams/NewStreamScreen.tsx | 72 +- js/mobile/src/features/streams/StreamCard.tsx | 70 +- .../src/features/streams/StreamListScreen.tsx | 38 +- .../streams/VisibilityPickerSheet.tsx | 89 +- js/mobile/src/firebase.ts | 10 +- js/mobile/src/hooks/use-channel.ts | 30 +- js/mobile/src/hooks/use-event.ts | 2 +- js/mobile/src/hooks/use-networks.ts | 8 +- js/mobile/src/hooks/use-particle.ts | 70 +- js/mobile/src/hooks/use-stream-particles.ts | 46 +- js/mobile/src/hooks/use-stream-playback.ts | 102 +- js/mobile/src/hooks/use-suspend-playback.ts | 4 +- .../src/hooks/use-transcript-playback.ts | 6 +- js/mobile/src/lib/errors.ts | 38 +- js/mobile/src/lib/firestore-particles.ts | 76 +- js/mobile/src/lib/humans.ts | 8 +- js/mobile/src/lib/notification-routing.ts | 20 +- js/mobile/src/lib/particle-path.ts | 8 +- js/mobile/src/lib/push-notifications.ts | 36 +- js/mobile/src/lib/pusher-client.ts | 50 +- js/mobile/src/lib/pusher-provider.tsx | 38 +- js/mobile/src/lib/query-client.ts | 18 +- js/mobile/src/lib/random-name.ts | 68 +- js/mobile/src/lib/recording-audio-session.ts | 6 +- js/mobile/src/lib/stream-visibility.ts | 14 +- js/mobile/src/lib/time-utils.ts | 5 +- js/mobile/src/lib/upload.ts | 47 +- js/mobile/src/lib/utils.ts | 6 +- js/mobile/src/navigation/RootNavigator.tsx | 34 +- js/mobile/src/navigation/types.ts | 6 +- js/mobile/src/stores/auth-store.ts | 47 +- js/mobile/src/stores/playback-pause-store.ts | 2 +- js/mobile/tailwind.config.js | 64 +- js/mobile/tsconfig.json | 18 +- js/mobile/yarn.lock | 1311 ++++++++++++++++- 258 files changed, 7822 insertions(+), 5195 deletions(-) create mode 100644 js/desktop/.prettierrc create mode 100644 js/desktop/src/hooks/use-object-url.ts create mode 100644 js/mobile/.prettierignore create mode 100644 js/mobile/.prettierrc create mode 100644 js/mobile/eslint.config.js diff --git a/js/desktop/.eslintrc.json b/js/desktop/.eslintrc.json index a5691c8..9506742 100644 --- a/js/desktop/.eslintrc.json +++ b/js/desktop/.eslintrc.json @@ -4,6 +4,21 @@ "es6": true, "node": true }, + "settings": { + "import/resolver": { + "typescript": {} + } + }, + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] + }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", @@ -11,7 +26,8 @@ "plugin:import/recommended", "plugin:import/electron", "plugin:import/typescript", - "plugin:react-hooks/recommended" + "plugin:react-hooks/recommended", + "prettier" ], "parser": "@typescript-eslint/parser" } diff --git a/js/desktop/.prettierrc b/js/desktop/.prettierrc new file mode 100644 index 0000000..2ff2460 --- /dev/null +++ b/js/desktop/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "jsxSingleQuote": false +} diff --git a/js/desktop/forge.config.ts b/js/desktop/forge.config.ts index e2381bd..d0af626 100644 --- a/js/desktop/forge.config.ts +++ b/js/desktop/forge.config.ts @@ -6,6 +6,7 @@ import { MakerRpm } from '@electron-forge/maker-rpm'; import { VitePlugin } from '@electron-forge/plugin-vite'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; +import type { SignToolOptions } from '@electron/windows-sign'; // Windows code signing via Azure Trusted Signing. Dormant unless the workflow // provisions the dlib + metadata file and exports these env vars, so local @@ -16,7 +17,7 @@ import { FuseV1Options, FuseVersion } from '@electron/fuses'; const windowsSign = process.env.AZURE_METADATA_JSON_PATH ? { debug: true, signToolPath: process.env.SIGNTOOL_PATH, - hashes: ['sha256' as any], + hashes: ['sha256'] as SignToolOptions['hashes'], timestampServer: 'http://timestamp.acs.microsoft.com', signWithParams: [ '/v', diff --git a/js/desktop/package.json b/js/desktop/package.json index 624a475..a4a4177 100644 --- a/js/desktop/package.json +++ b/js/desktop/package.json @@ -14,6 +14,8 @@ "publish:mac": "echo '\n⚠️ Have you bumped the version in package.json? (current: '$(node -p \"require('./package.json').version\")') [y/N]' && read -r answer && [ \"$answer\" = \"y\" ] && APP_ENV=prod electron-forge publish --arch=arm64 && APP_ENV=prod electron-forge publish --arch=x64", "invalidate-gcs-cache": "gsutil setmeta -h 'Cache-Control:no-cache, no-store, must-revalidate' gs://flowy-releases/llink/darwin/arm64/RELEASES.json && gsutil setmeta -h 'Cache-Control:no-cache, no-store, must-revalidate' gs://flowy-releases/llink/darwin/x64/RELEASES.json && gsutil setmeta -h 'Cache-Control:no-cache, no-store, must-revalidate' gs://flowy-releases/llink/win32/x64/RELEASES", "lint": "eslint --ext .ts,.tsx .", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "compile": "npx tsc --noEmit 2>&1 | grep '^src/'", "web:dev": "cross-env APP_ENV=dev vite --config vite.web.config.mts", "web:build": "cross-env APP_ENV=prod vite build --config vite.web.config.mts", @@ -44,15 +46,17 @@ "@types/node": "^25.3.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", "@vitejs/plugin-react": "^5.1.4", "cross-env": "^10.1.0", "electron": "40.6.0", "eslint": "^8.57.1", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react-hooks": "^7.0.1", + "prettier": "^3.8.3", "typescript": "^5.9.3", + "typescript-eslint": "^8.60.0", "vite": "^5.4.21" }, "dependencies": { diff --git a/js/desktop/src/App.tsx b/js/desktop/src/App.tsx index aca5249..b31e7c4 100644 --- a/js/desktop/src/App.tsx +++ b/js/desktop/src/App.tsx @@ -1,27 +1,27 @@ -import { useEffect } from "react"; -import { Routes, Route, useNavigate } from "react-router-dom"; -import { RouterShell } from "@/lib/router-shell"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { useAuthStore } from "@/stores/auth-store"; -import { LoginPage } from "@/features/auth/login-page"; +import { useEffect } from 'react'; +import { Routes, Route, useNavigate } from 'react-router-dom'; +import { RouterShell } from '@/lib/router-shell'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { useAuthStore } from '@/stores/auth-store'; +import { LoginPage } from '@/features/auth/login-page'; import { QueryClientProvider } from '@tanstack/react-query'; -import SettingsPage from "@/features/settings-page"; -import AudioVideoSettingsPage from "@/features/settings/audio-video-settings-page"; -import NetworkSelector from "@/features/network-selector"; -import NetworkRoot from "@/features/network-root"; -import ParticleViewResolver from "@/features/particles/particle-view-resolver"; -import Layout from "@/features/layout"; -import NetworkSettingsPage from "@/features/network-settings"; -import { Toaster } from "@/components/ui/sonner"; -import { PusherProvider } from "@/lib/pusher-provider"; -import { createQueryClient } from "@/lib/query-client"; +import SettingsPage from '@/features/settings-page'; +import AudioVideoSettingsPage from '@/features/settings/audio-video-settings-page'; +import NetworkSelector from '@/features/network-selector'; +import NetworkRoot from '@/features/network-root'; +import ParticleViewResolver from '@/features/particles/particle-view-resolver'; +import Layout from '@/features/layout'; +import NetworkSettingsPage from '@/features/network-settings'; +import { Toaster } from '@/components/ui/sonner'; +import { PusherProvider } from '@/lib/pusher-provider'; +import { createQueryClient } from '@/lib/query-client'; import { RouteErrorBoundary, TopLevelErrorBoundary, -} from "@/components/app-error-boundary"; -import { SoundEffectsProvider } from "@/lib/sound-effects/sound-effects-provider"; -import { platform } from "@/lib/platform"; -import { InAppAutoplayCard } from "@/components/in-app-autoplay-card"; +} from '@/components/app-error-boundary'; +import { SoundEffectsProvider } from '@/lib/sound-effects/sound-effects-provider'; +import { platform } from '@/lib/platform'; +import { InAppAutoplayCard } from '@/components/in-app-autoplay-card'; const queryClient = createQueryClient(); @@ -33,7 +33,7 @@ const App = () => { restoreSession(); }, [restoreSession]); - if (status === "idle" || status === "restoring") { + if (status === 'idle' || status === 'restoring') { return (

Loading...

@@ -41,7 +41,7 @@ const App = () => { ); } - if (status !== "authenticated") { + if (status !== 'authenticated') { return ; } @@ -72,12 +72,29 @@ function AuthenticatedApp() { } /> - } /> + } + /> - } /> + + + + } + /> - } /> + + + + } + /> } /> } /> diff --git a/js/desktop/src/api/client.ts b/js/desktop/src/api/client.ts index 08d5835..f62ab6b 100644 --- a/js/desktop/src/api/client.ts +++ b/js/desktop/src/api/client.ts @@ -1,7 +1,7 @@ -import { appConfig } from "@/config/env"; -import { useSessionStore } from "@/stores/session-store"; -import { ApiError } from "@/lib/errors"; -import type { z } from "zod"; +import { appConfig } from '@/config/env'; +import { useSessionStore } from '@/stores/session-store'; +import { ApiError } from '@/lib/errors'; +import type { z } from 'zod'; import { BillingStatusSchema, CheckoutSessionResponseSchema, @@ -16,7 +16,7 @@ import { PortalSessionResponseSchema, PrepareUploadResponseSchema, SignInResponseSchema, -} from "./types"; +} from './types'; import type { AcceptInvitationRequest, AddMembersRequest, @@ -26,8 +26,8 @@ import type { RequestCodeRequest, RevokeInvitationRequest, SignInRequest, -} from "./types"; -import type { LinkMetadata } from "@/lib/link-metadata"; +} from './types'; +import type { LinkMetadata } from '@/lib/link-metadata'; interface ApiClientConfig { baseUrl: string; @@ -50,12 +50,12 @@ class ApiClient { const headers: Record = {}; if (body) { - headers["Content-Type"] = "application/json"; + headers['Content-Type'] = 'application/json'; } const token = this.config.getToken(); if (token) { - headers["Authorization"] = `Bearer ${token}`; + headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${this.config.baseUrl}${path}`, { @@ -66,11 +66,11 @@ class ApiClient { if (response.status === 401) { this.config.onUnauthorized(); - throw new ApiError(401, "Unauthorized"); + throw new ApiError(401, 'Unauthorized'); } if (!response.ok) { - const text = await response.text().catch(() => "Unknown error"); + const text = await response.text().catch(() => 'Unknown error'); throw new ApiError(response.status, text); } @@ -99,43 +99,42 @@ class ApiClient { // --- Auth --- async requestCode(data: RequestCodeRequest): Promise { - await this.requestVoid("POST", "/auth/request-code", data); + await this.requestVoid('POST', '/auth/request-code', data); } async signIn(data: SignInRequest) { - return this.request(SignInResponseSchema, "POST", "/auth/sign-in", data); + return this.request(SignInResponseSchema, 'POST', '/auth/sign-in', data); } async me() { - return this.request(HumanSchema, "GET", "/auth/me"); + return this.request(HumanSchema, 'GET', '/auth/me'); } async signOut(): Promise { - await this.requestVoid("POST", "/auth/sign-out"); + await this.requestVoid('POST', '/auth/sign-out'); } async getFirebaseToken() { return this.request( FirebaseTokenResponseSchema, - "POST", - "/auth/firebase-token", + 'POST', + '/auth/firebase-token', ); } // TODO: security: require passing in the particle id once api deprecates this async getParticleDownloadUrl(objectId: string): Promise { - const response = await this.fetch( - "GET", - `/particles/${objectId}/download`, - ); + const response = await this.fetch('GET', `/particles/${objectId}/download`); const data = await response.json(); return data.url; } // --- Settings --- - async updateSettings(data: { email_notifications_enabled?: boolean }): Promise { - await this.requestVoid("PATCH", "/humans/me/settings", data); + async updateSettings(data: { + email_notifications_enabled?: boolean; + }): Promise { + await this.requestVoid('PATCH', '/humans/me/settings', data); } // --- Depot --- @@ -143,8 +142,8 @@ class ApiClient { async prepareUpload(data: PrepareUploadRequest) { return this.request( PrepareUploadResponseSchema, - "POST", - "/depot/upload", + 'POST', + '/depot/upload', data, ); } @@ -152,41 +151,32 @@ class ApiClient { async confirmUpload(objectId: string) { return this.request( DepotObjectSchema, - "POST", + 'POST', `/depot/objects/${objectId}/confirm`, ); } - // --- Networks --- async listNetworks() { - return this.request( - ListNetworksResponseSchema, - "GET", - "/networks", - ); + return this.request(ListNetworksResponseSchema, 'GET', '/networks'); } async createNetwork(data: CreateNetworkRequest) { - return this.request(NetworkSchema, "POST", "/networks", data); + return this.request(NetworkSchema, 'POST', '/networks', data); } async getNetwork(id: string) { - return this.request(NetworkSchema, "GET", `/networks/${id}`); + return this.request(NetworkSchema, 'GET', `/networks/${id}`); } async addMembers(networkId: string, data: AddMembersRequest): Promise { - await this.requestVoid( - "POST", - `/networks/${networkId}/members`, - data, - ); + await this.requestVoid('POST', `/networks/${networkId}/members`, data); } async removeMember(networkId: string, humanId: string): Promise { await this.requestVoid( - "DELETE", + 'DELETE', `/networks/${networkId}/members/${humanId}`, ); } @@ -196,31 +186,39 @@ class ApiClient { async listNetworkInvitations(networkId: string) { return this.request( ListInvitationsResponseSchema, - "GET", + 'GET', `/networks/${networkId}/invitations`, ); } async listMyInvitations() { - return this.request( - ListInvitationsResponseSchema, - "GET", - "/invitations", - ); + return this.request(ListInvitationsResponseSchema, 'GET', '/invitations'); } async acceptInvitation(data: AcceptInvitationRequest): Promise { - await this.requestVoid("POST", "/invitations/accept", data); + await this.requestVoid('POST', '/invitations/accept', data); } - async revokeInvitation(networkId: string, data: RevokeInvitationRequest): Promise { - await this.requestVoid("DELETE", `/networks/${networkId}/invitations`, data); + async revokeInvitation( + networkId: string, + data: RevokeInvitationRequest, + ): Promise { + await this.requestVoid( + 'DELETE', + `/networks/${networkId}/invitations`, + data, + ); } // --- LiveKit --- async getLivekitToken(networkId: string, streamId: string) { - return this.request(GetLivekitTokenResponseSchema, "POST", "/livekit/token", { network_id: networkId, stream_id: streamId }); + return this.request( + GetLivekitTokenResponseSchema, + 'POST', + '/livekit/token', + { network_id: networkId, stream_id: streamId }, + ); } // --- Billing (network admin only) --- @@ -228,7 +226,7 @@ class ApiClient { async getNetworkBilling(networkId: string) { return this.request( BillingStatusSchema, - "GET", + 'GET', `/networks/${networkId}/billing`, ); } @@ -236,7 +234,7 @@ class ApiClient { async createCheckoutSession(networkId: string, cadence: BillingCadence) { return this.request( CheckoutSessionResponseSchema, - "POST", + 'POST', `/networks/${networkId}/billing/checkout-session`, { cadence }, ); @@ -245,7 +243,7 @@ class ApiClient { async createPortalSession(networkId: string) { return this.request( PortalSessionResponseSchema, - "POST", + 'POST', `/networks/${networkId}/billing/portal-session`, ); } @@ -253,7 +251,7 @@ class ApiClient { async getNetworkUsage(networkId: string) { return this.request( NetworkUsageSchema, - "GET", + 'GET', `/networks/${networkId}/usage`, ); } @@ -262,7 +260,7 @@ class ApiClient { async getLinkMetadata(url: string): Promise { const response = await this.fetch( - "GET", + 'GET', `/metadata?url=${encodeURIComponent(url)}`, ); return (await response.json()) as LinkMetadata | null; diff --git a/js/desktop/src/api/types.ts b/js/desktop/src/api/types.ts index 1ab02d1..7410cbb 100644 --- a/js/desktop/src/api/types.ts +++ b/js/desktop/src/api/types.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from 'zod'; export const HumanSchema = z.object({ id: z.string(), @@ -25,12 +25,12 @@ export type ListNetworksResponse = z.infer; // --- Network request/response types --- -const CreateNetworkRequestSchema = z.object({ +export const CreateNetworkRequestSchema = z.object({ name: z.string(), }); export type CreateNetworkRequest = z.infer; -const AddMembersRequestSchema = z.object({ +export const AddMembersRequestSchema = z.object({ email_addresses: z.array(z.string().email()), }); export type AddMembersRequest = z.infer; @@ -52,7 +52,7 @@ export type RevokeInvitationRequest = { email: string }; // --- Depot types --- -const PrepareUploadRequestSchema = z.object({ +export const PrepareUploadRequestSchema = z.object({ network_id: z.string(), name: z.string(), content_type: z.string(), @@ -122,7 +122,7 @@ export const MediaPropertiesSchema = z.object({ duration_ms: z.number(), size_bytes: z.number(), transcript: TranscriptSchema.optional(), - source: z.enum(["camera", "screen"]).optional(), + source: z.enum(['camera', 'screen']).optional(), // Set by the particle processor worker once an iOS-playable MP4/m4a variant // has been produced from a non-iOS-playable original (e.g. WebM from desktop). // When present, clients should prefer these over object_id/mime_type for playback. @@ -162,7 +162,9 @@ export type PaperProperties = z.infer; // --- Reactions --- -export const ReactionsSchema = z.record(z.string(), z.array(z.string())).optional(); +export const ReactionsSchema = z + .record(z.string(), z.array(z.string())) + .optional(); export type Reactions = z.infer; // --- Tombstone (soft-delete) --- @@ -175,7 +177,15 @@ const TombstoneFields = { deleted_by_human_id: z.string().optional(), }; -export const REACTION_EMOJIS = ["\u{1F44D}", "\u{2764}\u{FE0F}", "\u{1F525}", "\u{1F440}", "\u{2705}", "\u{2753}", "\u{1F602}"] as const; +export const REACTION_EMOJIS = [ + '\u{1F44D}', + '\u{2764}\u{FE0F}', + '\u{1F525}', + '\u{1F440}', + '\u{2705}', + '\u{2753}', + '\u{1F602}', +] as const; export interface ParticlePropertiesMap { stream: StreamProperties; @@ -196,9 +206,9 @@ const ParticleBaseSchema = z.object({ updated_at: z.coerce.date().optional(), }); -export const ParticleSchema = z.discriminatedUnion("type", [ +export const ParticleSchema = z.discriminatedUnion('type', [ ParticleBaseSchema.extend({ - type: z.literal("stream"), + type: z.literal('stream'), properties: StreamPropertiesSchema, // e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John // e.g. ["network:xywx"] - visible to everyone in the network @@ -210,27 +220,53 @@ export const ParticleSchema = z.discriminatedUnion("type", [ last_child_created_at: z.coerce.date().optional(), // Array of humanIds currently in the huddle (updated via LiveKit webhooks) huddle_active_participants: z.array(z.string()).optional(), - status: z.enum(["open", "closed"]).optional(), + status: z.enum(['open', 'closed']).optional(), }), ParticleBaseSchema.extend({ - type: z.literal("folder"), properties: FolderPropertiesSchema, + type: z.literal('folder'), + properties: FolderPropertiesSchema, // e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John // e.g. ["network:123"] - visible to everyone in the network visible_to: z.array(z.string()), }), - ParticleBaseSchema.extend({ type: z.literal("media"), properties: MediaPropertiesSchema, reactions: ReactionsSchema, ...TombstoneFields }), - ParticleBaseSchema.extend({ type: z.literal("file"), properties: FilePropertiesSchema, ...TombstoneFields }), - ParticleBaseSchema.extend({ type: z.literal("text"), properties: TextPropertiesSchema, reactions: ReactionsSchema, ...TombstoneFields }), - ParticleBaseSchema.extend({ type: z.literal("quest"), properties: QuestPropertiesSchema, ...TombstoneFields }), - ParticleBaseSchema.extend({ type: z.literal("paper"), properties: PaperPropertiesSchema, ...TombstoneFields }), + ParticleBaseSchema.extend({ + type: z.literal('media'), + properties: MediaPropertiesSchema, + reactions: ReactionsSchema, + ...TombstoneFields, + }), + ParticleBaseSchema.extend({ + type: z.literal('file'), + properties: FilePropertiesSchema, + ...TombstoneFields, + }), + ParticleBaseSchema.extend({ + type: z.literal('text'), + properties: TextPropertiesSchema, + reactions: ReactionsSchema, + ...TombstoneFields, + }), + ParticleBaseSchema.extend({ + type: z.literal('quest'), + properties: QuestPropertiesSchema, + ...TombstoneFields, + }), + ParticleBaseSchema.extend({ + type: z.literal('paper'), + properties: PaperPropertiesSchema, + ...TombstoneFields, + }), ]); export type Particle = z.infer; -export type ParticleType = Particle["type"]; +export type ParticleType = Particle['type']; /** Container types can have children subcollections */ -export const CONTAINER_TYPES: ReadonlySet = new Set(["stream", "folder"]); +export const CONTAINER_TYPES: ReadonlySet = new Set([ + 'stream', + 'folder', +]); export function isContainerType(type: ParticleType): boolean { return CONTAINER_TYPES.has(type); @@ -238,7 +274,7 @@ export function isContainerType(type: ParticleType): boolean { /** True when a non-container particle has been soft-deleted (tombstoned). */ export function isParticleDeleted(particle: Particle): boolean { - return "deleted_at" in particle && particle.deleted_at != null; + return 'deleted_at' in particle && particle.deleted_at != null; } // --- LiveKit types --- @@ -247,16 +283,18 @@ export const GetLivekitTokenResponseSchema = z.object({ token: z.string(), server_url: z.string(), }); -export type GetLivekitTokenResponse = z.infer; +export type GetLivekitTokenResponse = z.infer< + typeof GetLivekitTokenResponseSchema +>; // --- Auth types --- -const RequestCodeRequestSchema = z.object({ +export const RequestCodeRequestSchema = z.object({ email: z.string().email(), }); export type RequestCodeRequest = z.infer; -const SignInRequestSchema = z.object({ +export const SignInRequestSchema = z.object({ email: z.string().email(), code: z.string(), }); @@ -275,21 +313,21 @@ export type FirebaseTokenResponse = z.infer; // --- Billing types --- -export const BillingCadenceSchema = z.enum(["monthly", "annual"]); +export const BillingCadenceSchema = z.enum(['monthly', 'annual']); export type BillingCadence = z.infer; -export const NetworkPlanSchema = z.enum(["free", "pro"]); +export const NetworkPlanSchema = z.enum(['free', 'pro']); export type NetworkPlan = z.infer; // Mirrors Stripe subscription.status plus "active" as the default free-tier value. export const BillingPlanStatusSchema = z.enum([ - "active", - "trialing", - "past_due", - "canceled", - "incomplete", - "incomplete_expired", - "unpaid", + 'active', + 'trialing', + 'past_due', + 'canceled', + 'incomplete', + 'incomplete_expired', + 'unpaid', ]); export type BillingPlanStatus = z.infer; @@ -308,7 +346,9 @@ export type BillingStatus = z.infer; export const CheckoutSessionResponseSchema = z.object({ url: z.string().url(), }); -export type CheckoutSessionResponse = z.infer; +export type CheckoutSessionResponse = z.infer< + typeof CheckoutSessionResponseSchema +>; export const PortalSessionResponseSchema = z.object({ url: z.string().url(), diff --git a/js/desktop/src/assets.d.ts b/js/desktop/src/assets.d.ts index 198d866..05f7aec 100644 --- a/js/desktop/src/assets.d.ts +++ b/js/desktop/src/assets.d.ts @@ -1,9 +1,9 @@ -declare module "*.wav" { +declare module '*.wav' { const src: string; export default src; } -declare module "*.mp3" { +declare module '*.mp3' { const src: string; export default src; } diff --git a/js/desktop/src/autoplay_window/AutoplayApp.tsx b/js/desktop/src/autoplay_window/AutoplayApp.tsx index b6240ca..4331129 100644 --- a/js/desktop/src/autoplay_window/AutoplayApp.tsx +++ b/js/desktop/src/autoplay_window/AutoplayApp.tsx @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; -import type { AutoplayPayload } from "@/lib/autoplay-ipc"; -import { AutoplayCardContent } from "@/components/autoplay-card-content"; +import { useCallback, useEffect, useState } from 'react'; +import type { AutoplayPayload } from '@/lib/autoplay-ipc'; +import { AutoplayCardContent } from '@/components/autoplay-card-content'; export function AutoplayApp() { const [payload, setPayload] = useState(null); diff --git a/js/desktop/src/autoplay_window/renderer.tsx b/js/desktop/src/autoplay_window/renderer.tsx index f9493f7..29fdbf9 100644 --- a/js/desktop/src/autoplay_window/renderer.tsx +++ b/js/desktop/src/autoplay_window/renderer.tsx @@ -5,5 +5,7 @@ import '@/styles/globals.css'; initSentryRenderer(); -const root = createRoot(document.getElementById('root')!); +const container = document.getElementById('root'); +if (!container) throw new Error('Root element #root not found'); +const root = createRoot(container); root.render(); diff --git a/js/desktop/src/components/app-error-boundary.tsx b/js/desktop/src/components/app-error-boundary.tsx index 1265a01..5bbc4be 100644 --- a/js/desktop/src/components/app-error-boundary.tsx +++ b/js/desktop/src/components/app-error-boundary.tsx @@ -1,12 +1,12 @@ -import type { PropsWithChildren } from "react"; -import { ErrorBoundary } from "react-error-boundary"; -import { useLocation } from "react-router-dom"; -import { useQueryErrorResetBoundary } from "@tanstack/react-query"; -import { reportError } from "@/lib/errors"; +import type { PropsWithChildren } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useLocation } from 'react-router-dom'; +import { useQueryErrorResetBoundary } from '@tanstack/react-query'; +import { reportError } from '@/lib/errors'; import { RouteErrorFallback, TopLevelErrorFallback, -} from "@/components/error-fallback"; +} from '@/components/error-fallback'; /** Catches render crashes OUTSIDE the router so bootstrap failures still recover. */ export function TopLevelErrorBoundary({ children }: PropsWithChildren) { @@ -15,7 +15,7 @@ export function TopLevelErrorBoundary({ children }: PropsWithChildren) { FallbackComponent={TopLevelErrorFallback} onError={(error, info) => reportError(error, { - boundary: "top", + boundary: 'top', componentStack: info.componentStack, }) } @@ -37,7 +37,7 @@ export function RouteErrorBoundary({ children }: PropsWithChildren) { FallbackComponent={RouteErrorFallback} onError={(error, info) => reportError(error, { - boundary: "route", + boundary: 'route', pathname: location.pathname, componentStack: info.componentStack, }) diff --git a/js/desktop/src/components/audio/audio-level-bars.tsx b/js/desktop/src/components/audio/audio-level-bars.tsx index a333a83..b5a7881 100644 --- a/js/desktop/src/components/audio/audio-level-bars.tsx +++ b/js/desktop/src/components/audio/audio-level-bars.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef } from 'react'; interface AudioLevelBarsProps { sourceNode: AudioNode; @@ -83,7 +83,8 @@ export function AudioLevelBars({ sourceNode }: AudioLevelBarsProps) { smoothedLevel <= threshold ? 0 : Math.min(1, (smoothedLevel - threshold) / (1 - threshold)); - const height = MIN_HEIGHT_PX + barLevel * (MAX_HEIGHT_PX - MIN_HEIGHT_PX); + const height = + MIN_HEIGHT_PX + barLevel * (MAX_HEIGHT_PX - MIN_HEIGHT_PX); el.style.height = `${height}px`; } diff --git a/js/desktop/src/components/audio/use-audio-source.ts b/js/desktop/src/components/audio/use-audio-source.ts index 7c4071b..958bb40 100644 --- a/js/desktop/src/components/audio/use-audio-source.ts +++ b/js/desktop/src/components/audio/use-audio-source.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from 'react'; interface AudioSource { sourceNode: AudioNode; @@ -17,46 +17,52 @@ export function useAudioSource( ): AudioSource | null { const [audioSource, setAudioSource] = useState(null); const elementSourceCache = useRef< - WeakMap + WeakMap< + HTMLAudioElement, + { sourceNode: MediaElementAudioSourceNode; ctx: AudioContext } + > >(new WeakMap()); useEffect(() => { - if (!source) { - setAudioSource(null); - return; - } + // Build the source node (creating an AudioContext as needed) plus optional + // teardown for contexts we own; the branches converge on one setState. + let result: AudioSource | null = null; + let cleanup: (() => void) | undefined; if (source instanceof MediaStream) { const ctx = new AudioContext(); ctx.resume(); const sourceNode = ctx.createMediaStreamSource(source); - setAudioSource({ sourceNode, ctx }); - - return () => { - ctx.close(); - }; + result = { sourceNode, ctx }; + cleanup = () => ctx.close(); + } else if (source) { + // HTMLAudioElement — createMediaElementSource can only be called once per + // element, so reuse a cached context/node when we have one. + const cached = elementSourceCache.current.get(source); + if (cached) { + cached.ctx.resume(); + result = cached; + } else { + const ctx = new AudioContext(); + ctx.resume(); + const sourceNode = ctx.createMediaElementSource(source); + // Connect element source to destination so audio is still audible + sourceNode.connect(ctx.destination); + elementSourceCache.current.set(source, { sourceNode, ctx }); + result = { sourceNode, ctx }; + cleanup = () => { + ctx.close(); + elementSourceCache.current.delete(source); + }; + } } - // HTMLAudioElement — createMediaElementSource can only be called once per element - const cached = elementSourceCache.current.get(source); - if (cached) { - cached.ctx.resume(); - setAudioSource(cached); - return; - } + // Publishing an imperatively-created Web Audio node — external-resource + // sync, not a re-render cascade. + // eslint-disable-next-line react-hooks/set-state-in-effect + setAudioSource(result); - const ctx = new AudioContext(); - ctx.resume(); - const sourceNode = ctx.createMediaElementSource(source); - // Connect element source to destination so audio is still audible - sourceNode.connect(ctx.destination); - elementSourceCache.current.set(source, { sourceNode, ctx }); - setAudioSource({ sourceNode, ctx }); - - return () => { - ctx.close(); - elementSourceCache.current.delete(source); - }; + return cleanup; }, [source]); return audioSource; diff --git a/js/desktop/src/components/autoplay-card-content.tsx b/js/desktop/src/components/autoplay-card-content.tsx index dc1847f..7453963 100644 --- a/js/desktop/src/components/autoplay-card-content.tsx +++ b/js/desktop/src/components/autoplay-card-content.tsx @@ -1,6 +1,6 @@ -import { useRef } from "react"; -import { X } from "lucide-react"; -import type { AutoplayPayload } from "@/lib/autoplay-ipc"; +import { useRef } from 'react'; +import { X } from 'lucide-react'; +import type { AutoplayPayload } from '@/lib/autoplay-ipc'; interface AutoplayCardContentProps { payload: AutoplayPayload; @@ -17,7 +17,7 @@ export function AutoplayCardContent({ }: AutoplayCardContentProps) { const mediaRef = useRef(null); - const isVideo = payload.mimeType.startsWith("video/"); + const isVideo = payload.mimeType.startsWith('video/'); const handleClick = () => { mediaRef.current?.pause(); @@ -56,7 +56,9 @@ export function AutoplayCardContent({
{payload.senderInitials}
-

{payload.senderName}

+

+ {payload.senderName} +

) : ( @@ -72,7 +74,9 @@ export function AutoplayCardContent({ {payload.senderInitials}
-

{payload.senderName}

+

+ {payload.senderName} +

Playing audio...

diff --git a/js/desktop/src/components/composing-indicator.tsx b/js/desktop/src/components/composing-indicator.tsx index d53e3c4..bb4aae5 100644 --- a/js/desktop/src/components/composing-indicator.tsx +++ b/js/desktop/src/components/composing-indicator.tsx @@ -1,6 +1,6 @@ -import type { Human } from "@/api/types"; -import { resolveHumanDisplay } from "@/lib/humans"; -import type { ComposingUser } from "@/features/particles/stream-presence-context"; +import type { Human } from '@/api/types'; +import { resolveHumanDisplay } from '@/lib/humans'; +import type { ComposingUser } from '@/features/particles/stream-presence-context'; interface ComposingIndicatorProps { users: ComposingUser[]; @@ -20,11 +20,11 @@ export function ComposingIndicator({ return (
{users.map((u) => { const { displayName } = resolveHumanDisplay(u.humanId, networkHumans); - const modeLabel = u.mode === "typing" ? "typing" : "recording"; + const modeLabel = u.mode === 'typing' ? 'typing' : 'recording'; return (
{ const handler = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); } }; - window.addEventListener("keydown", handler, { capture: true }); - return () => window.removeEventListener("keydown", handler, { capture: true }); + window.addEventListener('keydown', handler, { capture: true }); + return () => + window.removeEventListener('keydown', handler, { capture: true }); }, [onClose]); return createPortal( @@ -45,7 +46,7 @@ export function ConfirmDestructiveOverlay({ Esc - {" "} + {' '} to close
@@ -53,7 +54,12 @@ export function ConfirmDestructiveOverlay({
{description}
-
{toUserMessage(error)} - {appEnv === "dev" && error instanceof Error ? ( + {appEnv === 'dev' && error instanceof Error ? (
@@ -63,15 +63,11 @@ export function TopLevelErrorFallback({ resetErrorBoundary, }: FallbackProps) { const goHome = () => { - window.location.hash = "#/"; + window.location.hash = '#/'; resetErrorBoundary(); }; return ( - + ); } @@ -81,14 +77,10 @@ export function RouteErrorFallback({ }: FallbackProps) { const navigate = useNavigate(); const goHome = () => { - navigate("/"); + navigate('/'); resetErrorBoundary(); }; return ( - + ); } diff --git a/js/desktop/src/components/in-app-autoplay-card.tsx b/js/desktop/src/components/in-app-autoplay-card.tsx index 068cfd3..dd2926d 100644 --- a/js/desktop/src/components/in-app-autoplay-card.tsx +++ b/js/desktop/src/components/in-app-autoplay-card.tsx @@ -1,7 +1,7 @@ -import { useCallback } from "react"; -import { platform } from "@/lib/platform"; -import { useAutoplayPayloadStore } from "@/stores/autoplay-payload-store"; -import { AutoplayCardContent } from "@/components/autoplay-card-content"; +import { useCallback } from 'react'; +import { platform } from '@/lib/platform'; +import { useAutoplayPayloadStore } from '@/stores/autoplay-payload-store'; +import { AutoplayCardContent } from '@/components/autoplay-card-content'; /** * Bottom-right floating autoplay card used on the web client. The desktop @@ -21,10 +21,10 @@ export function InAppAutoplayCard() { setPendingNav({ networkId: payload.networkId, streamId: payload.streamId }); }, [payload, setPayload, setPendingNav]); - if (platform.kind !== "web") return null; + if (platform.kind !== 'web') return null; if (!payload) return null; - const isVideo = payload.mimeType.startsWith("video/"); + const isVideo = payload.mimeType.startsWith('video/'); return (
{ if (!open) return; const handler = (e: KeyboardEvent) => { - if (e.key === "Escape" || e.key === "?") { + if (e.key === 'Escape' || e.key === '?') { e.preventDefault(); e.stopPropagation(); onClose(); } }; - window.addEventListener("keydown", handler, { capture: true }); - return () => window.removeEventListener("keydown", handler, { capture: true }); + window.addEventListener('keydown', handler, { capture: true }); + return () => + window.removeEventListener('keydown', handler, { capture: true }); }, [open, onClose]); if (!open) return null; @@ -56,11 +57,11 @@ export function KeybindingsOverlay({ Esc - {" "} - or{" "} + {' '} + or{' '} ? - {" "} + {' '} to close
diff --git a/js/desktop/src/components/link-preview-card.tsx b/js/desktop/src/components/link-preview-card.tsx index ed042fe..3e3c368 100644 --- a/js/desktop/src/components/link-preview-card.tsx +++ b/js/desktop/src/components/link-preview-card.tsx @@ -1,9 +1,8 @@ -import { Copy, ExternalLink, Globe } from "lucide-react"; -import type { LinkMetadata } from "@/lib/link-metadata"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { platform } from "@/lib/platform"; +import { Copy, ExternalLink, Globe } from 'lucide-react'; +import type { LinkMetadata } from '@/lib/link-metadata'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; +import { platform } from '@/lib/platform'; interface LinkPreviewCardProps { metadata: LinkMetadata; @@ -35,7 +34,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) { alt="" className="h-32 w-full object-cover" onError={(e) => { - (e.target as HTMLImageElement).style.display = "none"; + (e.target as HTMLImageElement).style.display = 'none'; }} /> )} @@ -48,7 +47,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) { className="size-4 rounded-sm" onError={(e) => { (e.target as HTMLImageElement).replaceWith( - document.createElement("span"), + document.createElement('span'), ); }} /> diff --git a/js/desktop/src/components/relative-timestamp.tsx b/js/desktop/src/components/relative-timestamp.tsx index f926bdf..7efab0d 100644 --- a/js/desktop/src/components/relative-timestamp.tsx +++ b/js/desktop/src/components/relative-timestamp.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; -import { formatDistanceToNow } from "@/lib/time-utils"; +import { useEffect, useState } from 'react'; +import { formatDistanceToNow } from '@/lib/time-utils'; const TICK_MS = 30_000; diff --git a/js/desktop/src/components/screen-source-picker.tsx b/js/desktop/src/components/screen-source-picker.tsx index 7a11b15..4c3d44a 100644 --- a/js/desktop/src/components/screen-source-picker.tsx +++ b/js/desktop/src/components/screen-source-picker.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect } from 'react'; interface ScreenSourcePickerProps { title?: string; @@ -9,8 +9,8 @@ interface ScreenSourcePickerProps { } export function ScreenSourcePicker({ - title = "Select a screen", - confirmLabel = "Select", + title = 'Select a screen', + confirmLabel = 'Select', getSources, onSelect, onCancel, @@ -23,18 +23,15 @@ export function ScreenSourcePicker({ getSources().then((result) => { setSources(result); setLoading(false); + // Auto-select if there's only one source. + if (result.length === 1) { + setSelectedId(result[0].id); + } }); }, [getSources]); - // Auto-select if there's only one source - useEffect(() => { - if (!loading && sources.length === 1) { - setSelectedId(sources[0].id); - } - }, [loading, sources]); - - const screens = sources.filter((s) => s.id.startsWith("screen:")); - const windows = sources.filter((s) => s.id.startsWith("window:")); + const screens = sources.filter((s) => s.id.startsWith('screen:')); + const windows = sources.filter((s) => s.id.startsWith('window:')); return (
@@ -119,8 +116,8 @@ function SourceSection({ onClick={() => onSelect(source.id)} className={`overflow-hidden rounded-lg border-2 text-left transition-colors ${ selectedId === source.id - ? "border-blue-500 bg-zinc-800" - : "border-transparent bg-zinc-800/50 hover:border-zinc-600" + ? 'border-blue-500 bg-zinc-800' + : 'border-transparent bg-zinc-800/50 hover:border-zinc-600' }`} > & { - size?: "default" | "xs" | "sm" | "lg" + size?: 'default' | 'xs' | 'sm' | 'lg'; }) { return ( - ) + ); } function AvatarImage({ @@ -31,12 +31,12 @@ function AvatarImage({ - ) + ); } function AvatarFallback({ @@ -47,55 +47,58 @@ function AvatarFallback({ - ) + ); } -function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { +function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) { return ( svg]:hidden", - "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className + 'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none', + 'group-data-[size=xs]/avatar:size-1.5 group-data-[size=xs]/avatar:[&>svg]:hidden', + 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden', + 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2', + 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2', + className, )} {...props} /> - ) + ); } -function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { +function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) { return (
- ) + ); } function AvatarGroupCount({ className, ...props -}: React.ComponentProps<"div">) { +}: React.ComponentProps<'div'>) { return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)} + className={cn( + 'bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 group-has-data-[size=xs]/avatar-group:size-4 group-has-data-[size=xs]/avatar-group:text-[8px] [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2', + className, + )} {...props} /> - ) + ); } export { @@ -105,4 +108,4 @@ export { AvatarGroup, AvatarGroupCount, AvatarBadge, -} +}; diff --git a/js/desktop/src/components/ui/badge.tsx b/js/desktop/src/components/ui/badge.tsx index 086ee43..38e1271 100644 --- a/js/desktop/src/components/ui/badge.tsx +++ b/js/desktop/src/components/ui/badge.tsx @@ -1,36 +1,40 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Slot } from 'radix-ui'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const badgeVariants = cva( - "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge", + 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge', { variants: { variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", - outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + secondary: + 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', + destructive: + 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20', + outline: + 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', + ghost: + 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', + link: 'text-primary underline-offset-4 hover:underline', }, }, defaultVariants: { - variant: "default", + variant: 'default', }, - } -) + }, +); function Badge({ className, - variant = "default", + variant = 'default', asChild = false, ...props -}: React.ComponentProps<"span"> & +}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot.Root : "span" + const Comp = asChild ? Slot.Root : 'span'; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/js/desktop/src/components/ui/breadcrumb.tsx b/js/desktop/src/components/ui/breadcrumb.tsx index db9afc0..4280066 100644 --- a/js/desktop/src/components/ui/breadcrumb.tsx +++ b/js/desktop/src/components/ui/breadcrumb.tsx @@ -1,10 +1,10 @@ -import * as React from "react" -import { Slot } from "radix-ui" +import * as React from 'react'; +import { Slot } from 'radix-ui'; -import { cn } from "@/lib/utils" -import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" +import { cn } from '@/lib/utils'; +import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'; -function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { +function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) { return (