feat: list streams and story-mode catchup

This commit is contained in:
talksik
2026-02-21 09:46:04 -08:00
parent b5f90709de
commit 0cd74c0a8a
33 changed files with 2304 additions and 41 deletions
+81
View File
@@ -1,10 +1,17 @@
import { useSessionStore } from "@/stores/session-store";
import type {
CreateStreamParticleRequest,
CreateStreamRequest,
Human,
MarkSeenBatchRequest,
PrepareUploadRequest,
PrepareUploadResponse,
RequestCodeRequest,
SignInRequest,
SignInResponse,
StartupResponse,
Stream,
StreamParticle,
} from "./types";
export class ApiError extends Error {
@@ -67,6 +74,8 @@ class ApiClient {
return response.json() as Promise<T>;
}
// --- Auth ---
async requestCode(data: RequestCodeRequest): Promise<void> {
await this.request<void>("POST", "/auth/request-code", data);
}
@@ -83,9 +92,81 @@ class ApiClient {
await this.request<void>("POST", "/auth/sign-out");
}
// --- Startup ---
async startup(): Promise<StartupResponse> {
return this.request<StartupResponse>("GET", "/startup");
}
// --- Streams ---
async createStream(
networkId: string,
data: CreateStreamRequest,
): Promise<Stream> {
return this.request<Stream>(
"POST",
`/networks/${networkId}/streams`,
data,
);
}
async createStreamParticle(
streamId: string,
data: CreateStreamParticleRequest,
): Promise<StreamParticle> {
return this.request<StreamParticle>(
"POST",
`/streams/${streamId}/particles`,
data,
);
}
// --- Particles ---
async markSeen(particleId: string): Promise<void> {
await this.request<void>("POST", `/particles/${particleId}/seen`);
}
async markSeenBatch(data: MarkSeenBatchRequest): Promise<void> {
await this.request<void>("POST", "/particles/seen", data);
}
async getParticleDownloadUrl(particleId: string): Promise<string> {
const token = this.config.getToken();
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(
`${this.config.baseUrl}/particles/${particleId}/download`,
{ headers, redirect: "follow" },
);
if (response.status === 401) {
this.config.onUnauthorized();
throw new ApiError(401, "Unauthorized");
}
if (!response.ok) {
throw new ApiError(response.status, "Failed to get download URL");
}
return response.url;
}
// --- Depot ---
async prepareUpload(
data: PrepareUploadRequest,
): Promise<PrepareUploadResponse> {
return this.request<PrepareUploadResponse>("POST", "/depot/upload", data);
}
async confirmUpload(objectId: string): Promise<void> {
await this.request<void>("POST", `/depot/objects/${objectId}/confirm`);
}
}
export const apiClient = new ApiClient({
+151 -4
View File
@@ -1,11 +1,156 @@
// --- Core entities ---
export interface Human {
id: string;
id: string | null;
email: string;
email_prefix: string;
created_at: string;
updated_at: string;
created_at: string | null;
}
export type StreamStatus = "open" | "closed" | "unspecified";
export interface AckInfo {
email: string;
acked_at: string;
}
// --- Particle types ---
export type ParticleType =
| "media"
| "text"
| "quest"
| "paper"
| "file"
| "folder";
export interface MediaParticleData {
object_id: string;
duration_ms: number;
mime_type: string;
}
export interface TextParticleData {
content: string;
}
export interface QuestParticleData {
title: string;
description: string;
status?: string;
assigned_to?: string;
due_date?: string;
}
export interface PaperParticleData {
title: string;
content: string;
}
export interface FileParticleData {
object_id: string;
filename: string;
mime_type: string;
size: number;
}
export interface FolderParticleData {
name: string;
color?: string;
}
export interface ParticleDataMap {
media: MediaParticleData;
text: TextParticleData;
quest: QuestParticleData;
paper: PaperParticleData;
file: FileParticleData;
folder: FolderParticleData;
}
export function getParticleData<T extends ParticleType>(
particle: StreamParticle,
type: T,
): ParticleDataMap[T] {
return particle.data as ParticleDataMap[T];
}
export interface StreamParticle {
id: string;
type: ParticleType;
data: unknown;
created_by_email: string;
seen: boolean;
acks: AckInfo[];
updated_at: string;
created_at: string;
}
export interface Stream {
id: string;
name: string;
description: string;
status: StreamStatus;
members?: string[];
particles: StreamParticle[];
unseen_count: number;
}
export interface Network {
id: string;
name: string;
admin_human: Human;
humans: Human[];
open_stream_count: number;
open_stream_capacity: number;
created_at: string;
}
export interface NetworkWithStreams extends Network {
streams: Stream[];
}
// --- Depot types ---
export interface PrepareUploadRequest {
file_name: string;
content_type: string;
size_bytes: number;
}
export interface PrepareUploadResponse {
object: DepotObject;
upload_url: string;
}
export interface DepotObject {
id: string;
status: string;
content_type: string;
size_bytes: number;
created_at: string;
}
// --- Stream mutation types ---
export interface CreateStreamRequest {
name: string;
description: string;
visibility: "network_all" | "custom";
member_emails?: string[];
}
export interface CreateStreamParticleRequest {
type: ParticleType;
data: unknown;
}
export interface MarkSeenBatchRequest {
particle_ids: string[];
}
// --- Auth types ---
export interface RequestCodeRequest {
email: string;
}
@@ -20,6 +165,8 @@ export interface SignInResponse {
token: string;
}
// --- Startup ---
export interface StartupResponse {
networks: unknown[];
networks: NetworkWithStreams[];
}