From 52ff92083a391bc11ee1e6cdd59b93a2dd931daa Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 1 Jun 2026 14:17:58 -0700 Subject: [PATCH] format --- js/desktop/src/App.tsx | 67 ++- js/desktop/src/api/client.ts | 108 +++-- js/desktop/src/api/types.ts | 94 ++-- js/desktop/src/assets.d.ts | 4 +- .../src/autoplay_window/AutoplayApp.tsx | 6 +- .../src/components/app-error-boundary.tsx | 16 +- .../src/components/audio/audio-level-bars.tsx | 5 +- .../src/components/audio/use-audio-source.ts | 7 +- .../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 | 14 +- .../src/components/relative-timestamp.tsx | 4 +- .../src/components/screen-source-picker.tsx | 14 +- 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 | 81 ++-- 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 | 450 +++++++++++------- .../compose/compose-quota-indicator.tsx | 31 +- .../compose/configure-stream-step.tsx | 42 +- .../src/features/compose/markdown-editor.css | 5 +- .../src/features/compose/markdown-editor.tsx | 18 +- .../features/compose/recording-overlay.tsx | 87 ++-- .../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 | 72 +-- js/desktop/src/features/network-selector.tsx | 71 +-- js/desktop/src/features/network-settings.tsx | 70 +-- .../particles/delete-particle-overlay.tsx | 21 +- .../particles/deleted-particle-view.tsx | 14 +- .../particles/fallback-particle-view.tsx | 39 +- .../src/features/particles/folder-view.tsx | 6 +- .../particles/media-particle-view.tsx | 95 ++-- .../particles/particle-attachments.tsx | 47 +- .../features/particles/particle-list-view.tsx | 187 +++++--- .../features/particles/particle-preview.tsx | 75 ++- .../particles/particle-view-resolver.tsx | 34 +- .../particles/playback-page-indicator.tsx | 32 +- .../src/features/particles/reaction-bar.tsx | 83 +++- .../particles/rename-stream-overlay.tsx | 41 +- .../src/features/particles/stream-card.tsx | 298 ++++++------ .../particles/stream-context-menu.tsx | 20 +- .../particles/stream-members-overlay.tsx | 147 +++--- .../particles/stream-presence-context.tsx | 20 +- .../src/features/particles/stream-top-bar.tsx | 161 ++++--- .../src/features/particles/stream-view.tsx | 299 +++++++----- .../features/particles/text-edit-overlay.tsx | 31 +- .../features/particles/text-particle-view.tsx | 87 ++-- .../particles/text-reaction-input.tsx | 36 +- .../features/particles/transcript-overlay.tsx | 25 +- js/desktop/src/features/settings-page.tsx | 67 +-- .../settings/audio-video-settings-page.tsx | 40 +- js/desktop/src/firebase.ts | 6 +- js/desktop/src/hooks/use-billing.ts | 10 +- js/desktop/src/hooks/use-channel.ts | 22 +- 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 | 6 +- .../src/hooks/use-effective-device-id.ts | 2 +- js/desktop/src/hooks/use-file-input.ts | 21 +- js/desktop/src/hooks/use-link-metadata.ts | 10 +- js/desktop/src/hooks/use-media-devices.ts | 26 +- js/desktop/src/hooks/use-member-management.ts | 24 +- js/desktop/src/hooks/use-network-usage.ts | 18 +- js/desktop/src/hooks/use-networks.ts | 8 +- js/desktop/src/hooks/use-object-url.ts | 2 +- .../src/hooks/use-particle-attachments.ts | 16 +- js/desktop/src/hooks/use-particle.ts | 78 +-- js/desktop/src/hooks/use-playback-keys.ts | 35 +- .../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 | 63 +-- .../src/hooks/use-stream-keyboard-nav.ts | 22 +- .../src/hooks/use-stream-navigation-keys.ts | 22 +- js/desktop/src/hooks/use-stream-particles.ts | 39 +- js/desktop/src/hooks/use-stream-playback.ts | 182 ++++--- js/desktop/src/hooks/use-suspend-playback.ts | 4 +- .../src/hooks/use-transcript-playback.ts | 6 +- js/desktop/src/huddle_window/HuddleApp.tsx | 37 +- 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 | 127 ++--- js/desktop/src/lib/humans.ts | 8 +- js/desktop/src/lib/image-thumbnail.ts | 8 +- 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 | 57 ++- js/desktop/src/lib/pusher-client.ts | 61 +-- js/desktop/src/lib/pusher-provider.tsx | 14 +- 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 | 14 +- .../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 | 67 ++- js/desktop/src/main/ipc-utils.ts | 6 +- js/desktop/src/main/sentry.ts | 15 +- js/desktop/src/preload.ts | 48 +- .../ScreenRecordControlApp.tsx | 6 +- 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 +- 165 files changed, 3736 insertions(+), 2907 deletions(-) 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 8ac6a69..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(), @@ -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,7 +283,9 @@ 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 --- @@ -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/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 13ac7b5..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,7 +17,10 @@ export function useAudioSource( ): AudioSource | null { const [audioSource, setAudioSource] = useState(null); const elementSourceCache = useRef< - WeakMap + WeakMap< + HTMLAudioElement, + { sourceNode: MediaElementAudioSourceNode; ctx: AudioContext } + > >(new WeakMap()); useEffect(() => { 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 3738604..3e3c368 100644 --- a/js/desktop/src/components/link-preview-card.tsx +++ b/js/desktop/src/components/link-preview-card.tsx @@ -1,8 +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 { 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; @@ -34,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'; }} /> )} @@ -47,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 58e145d..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, @@ -30,8 +30,8 @@ export function ScreenSourcePicker({ }); }, [getSources]); - 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 (
@@ -116,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 (