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
This commit was merged in pull request #279.
This commit is contained in:
Arjun Patel
2026-06-12 12:26:49 -07:00
committed by GitHub
parent a358774106
commit 095b9876f9
37 changed files with 2384 additions and 791 deletions
+41 -12
View File
@@ -146,14 +146,21 @@ export const TextPropertiesSchema = z.object({
});
export type TextProperties = z.infer<typeof TextPropertiesSchema>;
export const QuestPropertiesSchema = z.object({
export const ChecklistItemSchema = z.object({
text: z.string(),
done: z.boolean(),
});
export type ChecklistItem = z.infer<typeof ChecklistItemSchema>;
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<typeof QuestPropertiesSchema>;
export type TaskProperties = z.infer<typeof TaskPropertiesSchema>;
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<typeof ParticleSchema>;
// 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<typeof UnknownParticleSchema>;
export type ParticleType = Particle['type'];
export type Particle = z.infer<typeof ParticleSchema> | UnknownParticle;
/** Particle types this client can read and write — excludes 'unknown'. */
export type ParticleType = z.infer<typeof ParticleSchema>['type'];
/** Container types can have children subcollections */
export const CONTAINER_TYPES: ReadonlySet<ParticleType> = new Set([
export const CONTAINER_TYPES: ReadonlySet<Particle['type']> = 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<Particle, { type: 'stream' }>) {
return stream.status !== 'closed';
}
// --- LiveKit types ---
export const GetLivekitTokenResponseSchema = z.object({