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 ( +