diff --git a/js/desktop/package.json b/js/desktop/package.json index e841565..a4a4177 100644 --- a/js/desktop/package.json +++ b/js/desktop/package.json @@ -15,6 +15,7 @@ "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", diff --git a/js/desktop/src/features/compose/compose-overlay.tsx b/js/desktop/src/features/compose/compose-overlay.tsx index f46a7bd..ccd4ef1 100644 --- a/js/desktop/src/features/compose/compose-overlay.tsx +++ b/js/desktop/src/features/compose/compose-overlay.tsx @@ -136,7 +136,6 @@ export function ComposeOverlay({ const cancel = useCallback(() => { setStepSync('idle'); setError(null); - setTextContent(''); setMediaStream(null); setReviewBlob(null); setReviewDurationMs(0); @@ -150,8 +149,7 @@ export function ComposeOverlay({ const addAttachments = useCallback( async (files: File[]) => { - const currentCount = attachments.length; - const available = MAX_ATTACHMENTS - currentCount; + const available = MAX_ATTACHMENTS - attachments.length; if (available <= 0) { toast.error(`Maximum ${MAX_ATTACHMENTS} attachments`); return; @@ -404,6 +402,7 @@ export function ComposeOverlay({ try { await createChildParticle(targetPath); cancel(); + setTextContent(''); } catch (err) { if (!handleQuotaError(err)) throw err; } @@ -413,7 +412,7 @@ export function ComposeOverlay({ setStepSync, createChildParticle, cancel, - handleQuotaError, + handleQuotaError ]); // New stream mode: create stream + first child @@ -434,8 +433,8 @@ export function ComposeOverlay({ const streamChildrenPath = particlePath(networkId, [streamId]); await createChildParticle(streamChildrenPath); - cancel(); + setTextContent(''); } catch (err) { if (!handleQuotaError(err)) throw err; } diff --git a/js/desktop/src/features/particles/text-reaction-input.tsx b/js/desktop/src/features/particles/text-reaction-input.tsx index 9736d40..c3dd3cc 100644 --- a/js/desktop/src/features/particles/text-reaction-input.tsx +++ b/js/desktop/src/features/particles/text-reaction-input.tsx @@ -23,10 +23,13 @@ export function TextReactionInput({ useSuspendPlayback(open, 'text-reaction'); - // Clear the input each time the popup opens. if (open !== prevOpen) { setPrevOpen(open); - if (open) setValue(''); + + // NOTE: perform side effects here when opening + if (open) { + setValue('') + } } useEffect(() => { @@ -44,6 +47,7 @@ export function TextReactionInput({ const handleSubmit = () => { if (!canSubmit) return; onSubmit(trimmed); + setValue(''); onClose(); }; diff --git a/js/desktop/src/features/settings/audio-video-settings-page.tsx b/js/desktop/src/features/settings/audio-video-settings-page.tsx index 8702156..930e4c7 100644 --- a/js/desktop/src/features/settings/audio-video-settings-page.tsx +++ b/js/desktop/src/features/settings/audio-video-settings-page.tsx @@ -36,8 +36,7 @@ function usePreviewStream( const [error, setError] = useState(null); const [prevEnabled, setPrevEnabled] = useState(enabled); - // Clear the preview when disabled; the effect below only manages the - // getUserMedia subscription. + // Clear the preview when disabled if (enabled !== prevEnabled) { setPrevEnabled(enabled); if (!enabled) { diff --git a/js/desktop/src/hooks/use-channel.ts b/js/desktop/src/hooks/use-channel.ts index 6da7008..39ff4e0 100644 --- a/js/desktop/src/hooks/use-channel.ts +++ b/js/desktop/src/hooks/use-channel.ts @@ -62,7 +62,6 @@ export function useChannel(channelId: string | null): UseChannelResult { client.off(channelId, 'leave', onLeave); client.off(channelId, 'message', onMessage); client.unsubscribe(channelId); - // Clear on teardown so a new channel doesn't briefly show stale data. setPresence([]); setMessages([]); }; diff --git a/js/desktop/src/hooks/use-media-devices.ts b/js/desktop/src/hooks/use-media-devices.ts index 38d5b10..334b2c9 100644 --- a/js/desktop/src/hooks/use-media-devices.ts +++ b/js/desktop/src/hooks/use-media-devices.ts @@ -75,8 +75,15 @@ export function useMediaDevices(): UseMediaDevicesResult { }, [refresh]); return { - audioInputs: devices.filter((d) => d.kind === 'audioinput'), - videoInputs: devices.filter((d) => d.kind === 'videoinput'), + // Before permission is granted, enumerateDevices returns placeholder + // entries with an empty deviceId — filter them out so consumers never + // render an empty-value , which Radix rejects. + audioInputs: devices.filter( + (d) => d.kind === 'audioinput' && d.deviceId !== '', + ), + videoInputs: devices.filter( + (d) => d.kind === 'videoinput' && d.deviceId !== '', + ), permissionState, refresh, requestLabels, diff --git a/js/desktop/src/hooks/use-object-url.ts b/js/desktop/src/hooks/use-object-url.ts index 3895516..7f5c70a 100644 --- a/js/desktop/src/hooks/use-object-url.ts +++ b/js/desktop/src/hooks/use-object-url.ts @@ -3,11 +3,6 @@ import { useEffect, useState } from 'react'; /** * Creates an object URL for a Blob/File and revokes it when the source changes * or the component unmounts. Returns null when given null. - * - * `createObjectURL` is an imperative side effect that must run inside an effect, - * so publishing the resulting URL to state here is genuine external-resource - * synchronization rather than a render cascade — hence the single, contained - * lint suppression below. */ export function useObjectUrl(source: Blob | null): string | null { const [url, setUrl] = useState(null); @@ -15,7 +10,7 @@ export function useObjectUrl(source: Blob | null): string | null { useEffect(() => { if (!source) return; const objectUrl = URL.createObjectURL(source); - // Intended external-resource publish (see hook doc), not a render cascade. + // Intended external-resource publish to sync, not a render cascade. // eslint-disable-next-line react-hooks/set-state-in-effect setUrl(objectUrl); return () => { diff --git a/js/desktop/src/hooks/use-particle.ts b/js/desktop/src/hooks/use-particle.ts index 325f896..bb917da 100644 --- a/js/desktop/src/hooks/use-particle.ts +++ b/js/desktop/src/hooks/use-particle.ts @@ -42,7 +42,6 @@ export function useLiveParticle(path: ParticlePath): UseLiveParticleResult { return () => { unsubscribe(); - // Reset on teardown so a new path doesn't flash the previous particle. setIsLoading(true); setError(null); setParticle(null); @@ -121,7 +120,6 @@ export function useLiveParticleChildren( return () => { unsubscribe(); - // Reset on teardown so a new path doesn't flash the previous children. setChildren([]); setError(null); setIsLoading(true); @@ -168,7 +166,6 @@ export function useLiveLatestChild( return () => { unsubscribe(); - // Reset on teardown so a new path doesn't flash the previous child. setIsLoading(true); setLatestChild(null); };