From 22ba0410a5140d99b6e2b77811211766ce69e1d5 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Fri, 12 Jun 2026 10:11:44 -0700 Subject: [PATCH] cleanup folders and events, and condense changes --- js/desktop/src/api/types.ts | 42 +++-- .../src/features/compose/compose-overlay.tsx | 33 +--- .../features/compose/event-compose-step.tsx | 126 -------------- .../src/features/particles/container-view.tsx | 87 ++++++---- .../particles/event-particle-view.tsx | 162 ------------------ .../particles/fallback-particle-view.tsx | 4 +- .../particles/particle-children-list.tsx | 17 +- .../features/particles/particle-preview.tsx | 21 --- .../particles/particle-view-resolver.tsx | 10 -- .../features/particles/stream-bottom-bar.tsx | 7 - .../particles/stream-context-menu.tsx | 50 ++++++ .../particles/stream-list-sidebar.tsx | 17 -- .../src/features/particles/stream-top-bar.tsx | 36 +++- .../src/features/particles/stream-view.tsx | 16 +- .../src/hooks/use-container-children.ts | 30 +++- js/desktop/src/hooks/use-dock-badge.ts | 4 +- js/desktop/src/lib/firestore-particles.ts | 68 ++++++-- js/desktop/src/lib/particle-display.ts | 46 +---- js/desktop/src/stores/compose-intent-store.ts | 2 - 19 files changed, 267 insertions(+), 511 deletions(-) delete mode 100644 js/desktop/src/features/compose/event-compose-step.tsx delete mode 100644 js/desktop/src/features/particles/event-particle-view.tsx create mode 100644 js/desktop/src/features/particles/stream-context-menu.tsx diff --git a/js/desktop/src/api/types.ts b/js/desktop/src/api/types.ts index 03807d1..48c114a 100644 --- a/js/desktop/src/api/types.ts +++ b/js/desktop/src/api/types.ts @@ -162,14 +162,6 @@ export const TaskPropertiesSchema = z.object({ }); export type TaskProperties = z.infer; -export const EventPropertiesSchema = z.object({ - title: z.string(), - start_at: z.coerce.date(), - end_at: z.coerce.date().optional(), - notes: z.string().optional(), -}); -export type EventProperties = z.infer; - export const PaperPropertiesSchema = z.object({ title: z.string(), content: z.string(), @@ -210,7 +202,6 @@ export interface ParticlePropertiesMap { file: FileProperties; text: TextProperties; task: TaskProperties; - event: EventProperties; paper: PaperProperties; } @@ -227,6 +218,9 @@ export const ParticleSchema = z.discriminatedUnion('type', [ ParticleBaseSchema.extend({ type: z.literal('stream'), properties: StreamPropertiesSchema, + // Open/closed lifecycle. Optional because some streams were created while + // the field was dropped — treat missing as 'open' (see isStreamOpen). + status: z.enum(['open', 'closed']).optional(), // e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John // e.g. ["network:xywx"] - visible to everyone in the network visible_to: z.array(z.string()), @@ -271,12 +265,6 @@ export const ParticleSchema = z.discriminatedUnion('type', [ reactions: ReactionsSchema, ...TombstoneFields, }), - ParticleBaseSchema.extend({ - type: z.literal('event'), - properties: EventPropertiesSchema, - reactions: ReactionsSchema, - ...TombstoneFields, - }), ParticleBaseSchema.extend({ type: z.literal('paper'), properties: PaperPropertiesSchema, @@ -284,17 +272,28 @@ export const ParticleSchema = z.discriminatedUnion('type', [ }), ]); -export type Particle = z.infer; +// Placeholder for docs whose `type` this client version doesn't recognize +// (e.g. a newer client wrote a particle type we don't ship yet). The converter +// maps them here instead of throwing, so they degrade to an "unsupported" +// view rather than breaking the whole subscription. +export const UnknownParticleSchema = ParticleBaseSchema.extend({ + type: z.literal('unknown'), + raw_type: z.string(), +}); +export type UnknownParticle = z.infer; -export type ParticleType = Particle['type']; +export type Particle = z.infer | UnknownParticle; + +/** Particle types this client can read and write — excludes 'unknown'. */ +export type ParticleType = z.infer['type']; /** Container types can have children subcollections */ -export const CONTAINER_TYPES: ReadonlySet = new Set([ +export const CONTAINER_TYPES: ReadonlySet = new Set([ 'stream', 'folder', ]); -export function isContainerType(type: ParticleType): boolean { +export function isContainerType(type: Particle['type']): boolean { return CONTAINER_TYPES.has(type); } @@ -303,6 +302,11 @@ export function isParticleDeleted(particle: Particle): boolean { return 'deleted_at' in particle && particle.deleted_at != null; } +/** Missing status counts as open (streams created while the field was dropped). */ +export function isStreamOpen(stream: Extract) { + return stream.status !== 'closed'; +} + // --- LiveKit types --- export const GetLivekitTokenResponseSchema = z.object({ diff --git a/js/desktop/src/features/compose/compose-overlay.tsx b/js/desktop/src/features/compose/compose-overlay.tsx index 7e2f751..1a525e5 100644 --- a/js/desktop/src/features/compose/compose-overlay.tsx +++ b/js/desktop/src/features/compose/compose-overlay.tsx @@ -20,9 +20,8 @@ import { ScreenSourcePicker } from '@/components/screen-source-picker'; import { KeyHint } from '@/components/key-hint'; import { TextComposeStep } from '@/features/compose/text-compose-step'; import { TaskComposeStep } from '@/features/compose/task-compose-step'; -import { EventComposeStep } from '@/features/compose/event-compose-step'; import { ConfigureContainerStep } from '@/features/compose/configure-container-step'; -import type { TaskProperties, EventProperties } from '@/api/types'; +import type { TaskProperties } from '@/api/types'; import { apiClient } from '@/api/client'; import { useMediaSettingsStore } from '@/stores/media-settings-store'; import { useMediaDevicesStore } from '@/stores/media-devices-store'; @@ -44,15 +43,13 @@ export type ComposeStep = | 'reviewing' | 'typing' | 'task' - | 'event' | 'configuring' | 'submitting'; type RecordingSource = 'media' | 'screen'; type PendingArtifact = - | { type: 'task'; properties: TaskProperties } - | { type: 'event'; properties: EventProperties }; + | { type: 'task'; properties: TaskProperties }; interface ComposeOverlayProps { networkId: string; @@ -106,8 +103,7 @@ export function ComposeOverlay({ const recordStartRef = useRef(0); const quotaExhaustedRef = useRef(quotaExhausted); const recordingSourceRef = useRef(recordingSource); - // Task/event captured by their compose steps, created on submit. A ref (not - // state) so submit handlers can set it and create in the same tick. + const pendingArtifactRef = useRef(null); useEffect(() => { quotaExhaustedRef.current = quotaExhausted; @@ -501,12 +497,8 @@ export function ComposeOverlay({ setStepSync('task'); }, [guardIdle, setStepSync]); - const handleEventIntent = useCallback(() => { - if (!guardIdle()) return; - setStepSync('event'); - }, [guardIdle, setStepSync]); - // Task/event submit: capture the artifact, then reuse the standard flow — + // Artifact submit: capture the artifact, then reuse the standard flow — // reply mode creates it under targetPath, root mode configures a stream // that will hold it as its first child. const handleArtifactSubmit = useCallback( @@ -577,9 +569,6 @@ export function ComposeOverlay({ case 'task': handleTaskIntent(); break; - case 'event': - handleEventIntent(); - break; case 'stop': handleStopIntent(); break; @@ -596,7 +585,6 @@ export function ComposeOverlay({ handleRecordIntent, handleTextIntent, handleTaskIntent, - handleEventIntent, handleStopIntent, handleCancelIntent, handleSendIntent, @@ -612,7 +600,6 @@ export function ComposeOverlay({ if ( currentStep === 'typing' || currentStep === 'task' || - currentStep === 'event' || currentStep === 'configuring' || currentStep === 'picking' ) { @@ -649,9 +636,6 @@ export function ComposeOverlay({ } else if (e.key === 'd' || e.key === 'D') { e.preventDefault(); handleTaskIntent(); - } else if (e.key === 'e' || e.key === 'E') { - e.preventDefault(); - handleEventIntent(); } break; } @@ -720,7 +704,6 @@ export function ComposeOverlay({ handleRecordIntent, handleTextIntent, handleTaskIntent, - handleEventIntent, handleStopIntent, handleCancelIntent, handleSendIntent, @@ -838,14 +821,6 @@ export function ComposeOverlay({ } /> )} - {step === 'event' && ( - - handleArtifactSubmit({ type: 'event', properties }) - } - /> - )} {!targetPath && step === 'configuring' && ( void; - onSubmit: (properties: EventProperties) => void; -} - -function nextFullHour(): Date { - const date = new Date(); - date.setMinutes(0, 0, 0); - date.setHours(date.getHours() + 1); - return date; -} - -const dateInputClass = - 'w-full rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white outline-none [color-scheme:dark] focus:border-white/30'; - -export function EventComposeStep({ - onCancel, - onSubmit, -}: EventComposeStepProps) { - const [title, setTitle] = useState(''); - const [startAt, setStartAt] = useState(() => - toDatetimeLocalValue(nextFullHour()), - ); - const [endAt, setEndAt] = useState(''); - const [notes, setNotes] = useState(''); - - const handleSubmit = useCallback(() => { - const trimmed = title.trim(); - if (!trimmed || !startAt) return; - onSubmit({ - title: trimmed, - start_at: new Date(startAt), - ...(endAt && { end_at: new Date(endAt) }), - ...(notes.trim() && { notes: notes.trim() }), - }); - }, [title, startAt, endAt, notes, onSubmit]); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - onCancel(); - } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleSubmit(); - } - }, - [onCancel, handleSubmit], - ); - - return ( -
-
-
- - setTitle(e.target.value)} - placeholder="What's happening?" - className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0" - /> -
- -
-
- - setStartAt(e.target.value)} - className={dateInputClass} - /> -
-
- - setEndAt(e.target.value)} - className={dateInputClass} - /> -
-
- -
- -