From 095b9876f91255687b18367a54a290688fface85 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Fri, 12 Jun 2026 12:26:49 -0700 Subject: [PATCH] feat: stream list view and tasks (#279) * first attempt at stream sidebar, tasks, and events * fix folder from root * cleanup folders and events, and condense changes * cleanup and add toggle for sidebar * cleanup * fix nits --- js/desktop/src/api/types.ts | 53 ++- js/desktop/src/components/ui/textarea.tsx | 18 + .../src/features/compose/compose-overlay.tsx | 73 +++- ...-step.tsx => configure-container-step.tsx} | 18 +- .../features/compose/task-compose-step.tsx | 125 ++++++ js/desktop/src/features/layout.tsx | 20 +- js/desktop/src/features/network-root.tsx | 121 +---- .../src/features/particles/container-view.tsx | 269 ++++++++++++ .../particles/delete-particle-overlay.tsx | 19 +- .../particles/fallback-particle-view.tsx | 14 +- .../src/features/particles/folder-view.tsx | 17 +- ...st-view.tsx => particle-children-list.tsx} | 287 +++++++----- .../features/particles/particle-preview.tsx | 36 +- .../particles/particle-view-resolver.tsx | 112 ++++- .../particles/rename-stream-overlay.tsx | 12 +- .../features/particles/stream-bottom-bar.tsx | 141 ++++++ .../particles/stream-context-menu.tsx | 6 +- .../particles/stream-list-sidebar.tsx | 227 ++++++++++ .../particles/stream-members-overlay.tsx | 9 +- .../src/features/particles/stream-top-bar.tsx | 66 ++- .../src/features/particles/stream-view.tsx | 412 ++++++++---------- .../features/particles/task-particle-view.tsx | 335 ++++++++++++++ .../features/particles/text-particle-view.tsx | 33 +- .../src/hooks/use-container-children.ts | 121 +++++ js/desktop/src/hooks/use-create-particle.ts | 11 +- js/desktop/src/hooks/use-fixed-dwell.ts | 48 ++ ...yboard-nav.ts => use-list-keyboard-nav.ts} | 46 +- js/desktop/src/hooks/use-live-draft-field.ts | 85 ++++ js/desktop/src/hooks/use-playback-keys.ts | 19 +- .../src/hooks/use-stream-navigation-keys.ts | 19 +- js/desktop/src/hooks/use-stream-particles.ts | 80 ---- js/desktop/src/hooks/use-stream-playback.ts | 15 + js/desktop/src/hooks/use-stream-view-mode.ts | 44 ++ js/desktop/src/lib/firestore-particles.ts | 131 ++++-- js/desktop/src/lib/particle-display.ts | 91 ++++ js/desktop/src/stores/compose-intent-store.ts | 9 +- js/mobile/src/lib/firestore-particles.ts | 33 +- 37 files changed, 2384 insertions(+), 791 deletions(-) create mode 100644 js/desktop/src/components/ui/textarea.tsx rename js/desktop/src/features/compose/{configure-stream-step.tsx => configure-container-step.tsx} (92%) create mode 100644 js/desktop/src/features/compose/task-compose-step.tsx create mode 100644 js/desktop/src/features/particles/container-view.tsx rename js/desktop/src/features/particles/{particle-list-view.tsx => particle-children-list.tsx} (61%) create mode 100644 js/desktop/src/features/particles/stream-bottom-bar.tsx create mode 100644 js/desktop/src/features/particles/stream-list-sidebar.tsx create mode 100644 js/desktop/src/features/particles/task-particle-view.tsx create mode 100644 js/desktop/src/hooks/use-container-children.ts create mode 100644 js/desktop/src/hooks/use-fixed-dwell.ts rename js/desktop/src/hooks/{use-stream-keyboard-nav.ts => use-list-keyboard-nav.ts} (68%) create mode 100644 js/desktop/src/hooks/use-live-draft-field.ts delete mode 100644 js/desktop/src/hooks/use-stream-particles.ts create mode 100644 js/desktop/src/hooks/use-stream-view-mode.ts create mode 100644 js/desktop/src/lib/particle-display.ts diff --git a/js/desktop/src/api/types.ts b/js/desktop/src/api/types.ts index ab63df4..48c114a 100644 --- a/js/desktop/src/api/types.ts +++ b/js/desktop/src/api/types.ts @@ -146,14 +146,21 @@ export const TextPropertiesSchema = z.object({ }); export type TextProperties = z.infer; -export const QuestPropertiesSchema = z.object({ +export const ChecklistItemSchema = z.object({ + text: z.string(), + done: z.boolean(), +}); +export type ChecklistItem = z.infer; + +export const TaskPropertiesSchema = z.object({ title: z.string(), - description: z.string(), - status: z.string().optional(), + notes: z.string().optional(), + checklist: z.array(ChecklistItemSchema).optional(), // humanId assigned_to: z.string().optional(), + done: z.boolean(), }); -export type QuestProperties = z.infer; +export type TaskProperties = z.infer; export const PaperPropertiesSchema = z.object({ title: z.string(), @@ -194,7 +201,7 @@ export interface ParticlePropertiesMap { media: MediaProperties; file: FileProperties; text: TextProperties; - quest: QuestProperties; + task: TaskProperties; paper: PaperProperties; } @@ -211,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()), @@ -221,7 +231,6 @@ 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(), }), ParticleBaseSchema.extend({ type: z.literal('folder'), @@ -229,6 +238,9 @@ export const ParticleSchema = z.discriminatedUnion('type', [ // 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()), + // Set to created_at on creation, bumped when children are added — keeps + // folders present in activity-ordered container queries. + last_child_created_at: z.coerce.date().optional(), }), ParticleBaseSchema.extend({ type: z.literal('media'), @@ -248,8 +260,9 @@ export const ParticleSchema = z.discriminatedUnion('type', [ ...TombstoneFields, }), ParticleBaseSchema.extend({ - type: z.literal('quest'), - properties: QuestPropertiesSchema, + type: z.literal('task'), + properties: TaskPropertiesSchema, + reactions: ReactionsSchema, ...TombstoneFields, }), ParticleBaseSchema.extend({ @@ -259,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); } @@ -278,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/components/ui/textarea.tsx b/js/desktop/src/components/ui/textarea.tsx new file mode 100644 index 0000000..46eab5e --- /dev/null +++ b/js/desktop/src/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { + return ( +