@marktiderman/genesis-chat 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +379 -13
  2. package/dist/chunk-A4YJX7KT.js +544 -0
  3. package/dist/chunk-A4YJX7KT.js.map +1 -0
  4. package/dist/chunk-AZS3HNLM.js +30 -0
  5. package/dist/chunk-AZS3HNLM.js.map +1 -0
  6. package/dist/chunk-BJCE5O7V.js +70 -0
  7. package/dist/chunk-BJCE5O7V.js.map +1 -0
  8. package/dist/chunk-GBJ3YI4E.js +966 -0
  9. package/dist/chunk-GBJ3YI4E.js.map +1 -0
  10. package/dist/chunk-JU2FAPBI.js +609 -0
  11. package/dist/chunk-JU2FAPBI.js.map +1 -0
  12. package/dist/chunk-VEPZROQB.js +187 -0
  13. package/dist/chunk-VEPZROQB.js.map +1 -0
  14. package/dist/{client-BtdALFCx.d.ts → client-DUIG1hum.d.ts} +187 -11
  15. package/dist/index.d.ts +78 -163
  16. package/dist/index.js +3 -2
  17. package/dist/native.d.ts +1939 -1
  18. package/dist/native.js +5023 -0
  19. package/dist/native.js.map +1 -1
  20. package/dist/outbox-B9NAWQ58.d.ts +219 -0
  21. package/dist/push.d.ts +206 -0
  22. package/dist/push.js +3 -0
  23. package/dist/push.js.map +1 -0
  24. package/dist/react.d.ts +176 -7
  25. package/dist/react.js +3 -3
  26. package/dist/server.d.ts +551 -21
  27. package/dist/server.js +976 -34
  28. package/dist/server.js.map +1 -1
  29. package/dist/thread-oPra7pkW.d.ts +57 -0
  30. package/dist/web.d.ts +335 -12
  31. package/dist/web.js +1784 -64
  32. package/dist/web.js.map +1 -1
  33. package/migrations/0002_genesis_chat_coaching.sql +934 -0
  34. package/migrations/0003_genesis_chat_search_threads.sql +85 -0
  35. package/migrations/tests/0002_genesis_chat_coaching.test.sql +269 -0
  36. package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
  37. package/package.json +78 -6
  38. package/src/__tests__/coaching-core.test.ts +334 -0
  39. package/src/__tests__/coaching-react.test.tsx +319 -0
  40. package/src/__tests__/coaching-web.test.tsx +521 -0
  41. package/src/__tests__/core.test.ts +91 -0
  42. package/src/__tests__/fake-xhr.ts +54 -0
  43. package/src/__tests__/outbox-hosted.test.ts +269 -0
  44. package/src/__tests__/outbox.test.ts +122 -0
  45. package/src/__tests__/realtime.test.ts +70 -0
  46. package/src/__tests__/review-279.test.ts +41 -0
  47. package/src/__tests__/review-fixes-core.test.ts +193 -0
  48. package/src/__tests__/review-fixes-web.test.tsx +405 -0
  49. package/src/__tests__/server-effects.test.ts +158 -0
  50. package/src/__tests__/server-fake-video.test.ts +81 -0
  51. package/src/__tests__/server-fakes.ts +96 -0
  52. package/src/__tests__/server-integrations.test.ts +247 -0
  53. package/src/__tests__/server-providers.test.ts +246 -0
  54. package/src/__tests__/server-runner.test.ts +277 -0
  55. package/src/__tests__/server-safety.test.ts +303 -0
  56. package/src/__tests__/web-markdown.test.tsx +88 -0
  57. package/src/__tests__/web.test.tsx +349 -0
  58. package/src/core/availability.ts +223 -0
  59. package/src/core/client.ts +783 -0
  60. package/src/core/extras.ts +106 -0
  61. package/src/core/media.ts +144 -0
  62. package/src/core/outbox.ts +635 -0
  63. package/src/core/thread.ts +171 -0
  64. package/src/core/transfer.ts +71 -0
  65. package/src/core/types.ts +328 -0
  66. package/src/index.ts +14 -0
  67. package/src/native/__tests__/auto-retry.test.tsx +128 -0
  68. package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
  69. package/src/native/__tests__/composer-state.test.ts +188 -0
  70. package/src/native/__tests__/composer.test.tsx +133 -0
  71. package/src/native/__tests__/emoji.test.tsx +254 -0
  72. package/src/native/__tests__/inbox-screen.test.tsx +48 -0
  73. package/src/native/__tests__/message-bubble.test.tsx +127 -0
  74. package/src/native/__tests__/organise-logic.test.ts +164 -0
  75. package/src/native/__tests__/organise-screens.test.tsx +424 -0
  76. package/src/native/__tests__/photo-viewer.test.tsx +226 -0
  77. package/src/native/__tests__/push-payload.test.ts +66 -0
  78. package/src/native/__tests__/push.test.ts +209 -0
  79. package/src/native/__tests__/recorder-machine.test.ts +96 -0
  80. package/src/native/__tests__/rows-actions.test.ts +76 -0
  81. package/src/native/__tests__/thread-layout.test.ts +41 -0
  82. package/src/native/__tests__/thread-screen.test.tsx +116 -0
  83. package/src/native/__tests__/tus.test.ts +154 -0
  84. package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
  85. package/src/native/__tests__/video-media.test.tsx +498 -0
  86. package/src/native/__tests__/video-recorder-state.test.ts +87 -0
  87. package/src/native/__tests__/video-upload.test.ts +227 -0
  88. package/src/native/access-error.tsx +31 -0
  89. package/src/native/actions.ts +42 -0
  90. package/src/native/attachment-cards.tsx +132 -0
  91. package/src/native/auto-retry.ts +69 -0
  92. package/src/native/availability.tsx +376 -0
  93. package/src/native/broadcast-sheet.tsx +193 -0
  94. package/src/native/composer-state.ts +302 -0
  95. package/src/native/composer.tsx +453 -0
  96. package/src/native/context.tsx +70 -0
  97. package/src/native/copy.ts +296 -0
  98. package/src/native/emoji-data.ts +21 -0
  99. package/src/native/emoji-picker.tsx +229 -0
  100. package/src/native/emoji.ts +118 -0
  101. package/src/native/icon-button.tsx +40 -0
  102. package/src/native/inbox-screen.tsx +122 -0
  103. package/src/native/index.ts +184 -0
  104. package/src/native/link-preview.tsx +104 -0
  105. package/src/native/media-colors.ts +15 -0
  106. package/src/native/media.tsx +187 -0
  107. package/src/native/message-actions-sheet.tsx +126 -0
  108. package/src/native/message-bubble.tsx +366 -0
  109. package/src/native/moderation.tsx +42 -0
  110. package/src/native/modules.ts +277 -0
  111. package/src/native/organise-copy.ts +195 -0
  112. package/src/native/photo-viewer.tsx +531 -0
  113. package/src/native/push.ts +375 -0
  114. package/src/native/recorder-machine.ts +176 -0
  115. package/src/native/rows.ts +81 -0
  116. package/src/native/schedule-sheet.tsx +328 -0
  117. package/src/native/search-screen.tsx +189 -0
  118. package/src/native/stack-options.ts +36 -0
  119. package/src/native/start-sheet.tsx +87 -0
  120. package/src/native/thread-screen.tsx +647 -0
  121. package/src/native/topics.tsx +234 -0
  122. package/src/native/tus.ts +282 -0
  123. package/src/native/use-composer.ts +71 -0
  124. package/src/native/use-jump.ts +91 -0
  125. package/src/native/use-voice-recorder.ts +234 -0
  126. package/src/native/video-player.tsx +410 -0
  127. package/src/native/video-recorder-state.ts +140 -0
  128. package/src/native/video-recorder.tsx +274 -0
  129. package/src/native/video-upload-tray.tsx +77 -0
  130. package/src/native/video-upload.ts +432 -0
  131. package/src/native/voice-player.tsx +301 -0
  132. package/src/react/index.tsx +894 -0
  133. package/src/react/previews.ts +51 -0
  134. package/src/server/assistant.ts +235 -0
  135. package/src/server/crypto.ts +86 -0
  136. package/src/server/expo.ts +119 -0
  137. package/src/server/fake-video.ts +171 -0
  138. package/src/server/handler.ts +32 -0
  139. package/src/server/index.ts +58 -0
  140. package/src/server/moderation.ts +155 -0
  141. package/src/server/mux.ts +290 -0
  142. package/src/server/notifications.ts +130 -0
  143. package/src/server/runner.ts +468 -0
  144. package/src/server/transcribers.ts +111 -0
  145. package/src/server/types.ts +171 -0
  146. package/src/server/unfurl.ts +480 -0
  147. package/src/server/upload.ts +158 -0
  148. package/src/web/attachments.tsx +379 -0
  149. package/src/web/availability.tsx +299 -0
  150. package/src/web/broadcast.tsx +166 -0
  151. package/src/web/bubble.tsx +585 -0
  152. package/src/web/cards.tsx +136 -0
  153. package/src/web/composer.tsx +571 -0
  154. package/src/web/emoji-picker.tsx +84 -0
  155. package/src/web/error-boundary.tsx +33 -0
  156. package/src/web/format.ts +55 -0
  157. package/src/web/inbox.tsx +196 -0
  158. package/src/web/index.ts +75 -0
  159. package/src/web/markdown.tsx +245 -0
  160. package/src/web/memory-client.ts +714 -0
  161. package/src/web/panel.tsx +135 -0
  162. package/src/web/schedule.tsx +204 -0
  163. package/src/web/search.tsx +129 -0
  164. package/src/web/thread-context.ts +64 -0
  165. package/src/web/thread.tsx +830 -0
  166. package/src/web/topics.tsx +160 -0
  167. package/src/web/use-voice-recorder.ts +264 -0
  168. package/src/web/video.tsx +516 -0
  169. package/src/web/voice-note.tsx +197 -0
  170. package/dist/chunk-27UTR5FW.js +0 -322
  171. package/dist/chunk-27UTR5FW.js.map +0 -1
  172. package/dist/chunk-JQXZUEEL.js +0 -525
  173. package/dist/chunk-JQXZUEEL.js.map +0 -1
  174. package/dist/chunk-SAY24E6V.js +0 -309
  175. package/dist/chunk-SAY24E6V.js.map +0 -1
@@ -2,7 +2,7 @@ import { SupabaseClient } from '@supabase/supabase-js';
2
2
 
3
3
  /**
4
4
  * @feat:chat
5
- * The chat contract: what the genesis_chat schema (migration 0001) stores, in
5
+ * The chat contract: what the genesis_chat schema (migrations 0001 and 0002) stores, in
6
6
  * the app's words. Everything here is plain data, shared by web, native and
7
7
  * the server.
8
8
  */
@@ -64,6 +64,11 @@ interface ChatMessage {
64
64
  createdAt: string;
65
65
  editedAt: string | null;
66
66
  deletedAt: string | null;
67
+ /**
68
+ * What moderation said (migration 0002): `hidden` messages reach only their sender and the
69
+ * conversation's observers; `flagged` ones are visible to everyone and reported to observers.
70
+ */
71
+ moderation?: "flagged" | "hidden" | null;
67
72
  attachments: ChatAttachment[];
68
73
  reactions: ChatReaction[];
69
74
  }
@@ -174,6 +179,8 @@ interface PendingMessage {
174
179
  createdAt: string;
175
180
  status: "sending" | "failed";
176
181
  error?: string;
182
+ /** The server refused it (not allowed, bad data): only a manual retry sends it again. */
183
+ permanent?: boolean;
177
184
  /** Local previews for files still uploading (object URLs or file URIs). */
178
185
  previews?: Record<number, string>;
179
186
  /** What each file is, in order, so a pending bubble can draw it (a voice note's duration and waveform, a file's name). */
@@ -186,6 +193,20 @@ interface PendingFile {
186
193
  meta?: Partial<AttachmentInput>;
187
194
  /** Not kept between launches (bytes with nowhere to save them): attach it again. */
188
195
  lost?: boolean;
196
+ /** How much of it has uploaded, 0 to 1, once the upload reports progress. */
197
+ progress?: number;
198
+ /** Going to the hosted video provider (Mux, Cloudflare) rather than storage. */
199
+ hosted?: boolean;
200
+ }
201
+ /**
202
+ * A hosted video upload that has started: the provider's upload id, sent with the message as
203
+ * `{ provider, providerId, status: "pending" }` while the bytes still go up. The provider's
204
+ * webhook marks the attachment ready.
205
+ */
206
+ interface HostedAttachment {
207
+ provider: "mux" | "cloudflare";
208
+ providerId: string;
209
+ status: "pending";
189
210
  }
190
211
  /** A live hint from Realtime: something changed; fetch to see what. */
191
212
  type ChatSignal = {
@@ -215,10 +236,101 @@ interface ChatActivity {
215
236
  activity: "typing" | "recording-voice" | "recording-video";
216
237
  at: number;
217
238
  }
239
+ /** A named sub-thread of a conversation (a cohort's lesson threads). Messages join one through `topic`. */
240
+ interface ChatTopic {
241
+ conversationId: string;
242
+ /** The key messages carry in `topic`. */
243
+ topic: string;
244
+ title: string;
245
+ position: number;
246
+ /** Set when closed: only owners, admins and observers post in it then. */
247
+ closedAt: string | null;
248
+ createdBy: string | null;
249
+ createdAt: string;
250
+ }
251
+ /** What `setTopic` changes; anything left out keeps its value (a new topic's title is its key). */
252
+ interface TopicInput {
253
+ title?: string;
254
+ position?: number;
255
+ closed?: boolean;
256
+ }
257
+ /** A message to send later, as its sender. Only its sender reads it. */
258
+ interface ScheduledMessage {
259
+ id: string;
260
+ conversationId: string;
261
+ clientId: string;
262
+ body: string;
263
+ attachments: AttachmentInput[];
264
+ replyToId: string | null;
265
+ topic: string | null;
266
+ mentions: string[];
267
+ silent: boolean;
268
+ sendAt: string;
269
+ sentMessageId: string | null;
270
+ cancelledAt: string | null;
271
+ /** Why it couldn't be sent when due (the sender left the conversation, say). */
272
+ error: string | null;
273
+ createdAt: string;
274
+ status: "scheduled" | "sent" | "cancelled" | "failed";
275
+ }
276
+ /** Between a minute and a year ahead. Files in `attachments` are uploaded first, as for `send`. */
277
+ interface ScheduleInput extends Omit<SendInput, "threadRootId" | "visibility"> {
278
+ sendAt: string | Date;
279
+ }
280
+ type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
281
+ /** Working hours per weekday as `[["09:00","17:00"]]`, 24-hour local times; a missing day is not working. An end at or before its start runs past midnight. */
282
+ type WorkingHours = Partial<Record<Weekday, [string, string][]>>;
283
+ /** A person's away message and working hours. */
284
+ interface ChatAvailability {
285
+ userId: string;
286
+ awayMessage: string | null;
287
+ /** Away until then; with an away message and no date, away until they clear it. */
288
+ awayUntil: string | null;
289
+ /** An IANA zone (`America/New_York`); hours are read in it. */
290
+ timeZone: string | null;
291
+ hours: WorkingHours | null;
292
+ }
293
+ type AvailabilityInput = Partial<Omit<ChatAvailability, "userId">>;
294
+ /** One search result. `snippet` marks the matched words with « and » (the text's own « and » arrive as ‹ and ›, so marks are always real). */
295
+ interface ChatSearchHit {
296
+ messageId: string;
297
+ conversationId: string;
298
+ seq: number;
299
+ senderId: string | null;
300
+ createdAt: string;
301
+ snippet: string;
302
+ /** Found in a voice note's or video's transcript, not the text. */
303
+ inTranscript: boolean;
304
+ /** The thread the message is a reply in (open that thread to show it); null on the main timeline. */
305
+ threadRootId: string | null;
306
+ }
307
+ /** Open Graph data for a link (fetched by the server's runner). Plain text: never render it as HTML. */
308
+ interface ChatLinkPreview {
309
+ url: string;
310
+ status: "ok" | "failed" | "blocked";
311
+ title: string | null;
312
+ description: string | null;
313
+ /** Always https when present. */
314
+ imageUrl: string | null;
315
+ siteName: string | null;
316
+ fetchedAt: string;
317
+ }
318
+
319
+ type UploadProgress = (sent: number, total: number) => void;
320
+ interface SendBytesOptions {
321
+ method?: "PUT" | "POST";
322
+ headers?: Record<string, string>;
323
+ signal?: AbortSignal;
324
+ onProgress?: UploadProgress;
325
+ /** For tests and runtimes without XMLHttpRequest. */
326
+ fetch?: typeof fetch;
327
+ }
328
+ /** Sends `body` to `url`; resolves with the response text, throws a ChatError on a non-2xx answer. */
329
+ declare function sendBytes(url: string, body: Blob | ArrayBuffer, opts?: SendBytesOptions): Promise<string>;
218
330
 
219
331
  /**
220
332
  * @feat:chat
221
- * The chat client: the app's own Supabase (migration 0001, the genesis_chat
333
+ * The chat client: the app's own Supabase (migrations 0001 and 0002, the genesis_chat
222
334
  * schema) under row level security. Reads are table selects; every write is a
223
335
  * genesis_chat function. Live updates are hints on private Realtime topics,
224
336
  * after which the client fetches again, so nothing is trusted from the wire.
@@ -239,10 +351,21 @@ declare class ChatError extends Error {
239
351
  get rateLimited(): boolean;
240
352
  /** Worth trying again: the network, not the database, said no. */
241
353
  get transient(): boolean;
354
+ /**
355
+ * Sending it again won't help: the database refused what was sent (not allowed 42501, bad
356
+ * data 22xxx such as 22023, a conflict 23xxx such as 23505, another refusal 42xxx, or a
357
+ * function's own `raise` P0001). A person may still retry by hand; nothing retries it alone.
358
+ */
359
+ get permanent(): boolean;
242
360
  }
243
361
  type Row = Record<string, unknown>;
244
362
  declare function toAttachment(r: Row): ChatAttachment;
245
363
  declare function toMessage(r: Row): ChatMessage;
364
+ declare function toTopic(r: Row): ChatTopic;
365
+ declare function toScheduled(r: Row): ScheduledMessage;
366
+ declare function toAvailability(r: Row): ChatAvailability;
367
+ declare function toSearchHit(r: Row): ChatSearchHit;
368
+ declare function toLinkPreview(r: Row): ChatLinkPreview;
246
369
  interface MessagePage {
247
370
  messages: ChatMessage[];
248
371
  /** More, older messages exist before the first one. */
@@ -255,14 +378,25 @@ interface ChatClientOptions {
255
378
  interface Subscription {
256
379
  unsubscribe(): void;
257
380
  }
381
+ interface ConversationHandlers {
382
+ onSignal: (s: ChatSignal) => void;
383
+ onActivity?: (a: ChatActivity) => void;
384
+ /** Every join, the first included: catch up on what arrived before the channel was live. */
385
+ onJoin?: () => void;
386
+ /** Re-joins only. Prefer `onJoin`. */
387
+ onReconnect?: () => void;
388
+ }
258
389
  interface ConversationChannel extends Subscription {
259
390
  /** Tell the others you are typing or recording (throttle to once every 3 seconds). */
260
391
  signal(activity: ChatActivity["activity"]): void;
261
392
  }
262
393
  declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOptions): {
263
394
  supabase: ChatSupabase;
395
+ /** Your conversations: one `kind`, several `kinds`, or all but `excludeKinds` (e.g. Messages without `support`). */
264
396
  inbox(opts?: {
265
397
  kind?: string;
398
+ kinds?: string[];
399
+ excludeKinds?: string[];
266
400
  archived?: boolean;
267
401
  limit?: number;
268
402
  before?: string;
@@ -272,7 +406,10 @@ declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOp
272
406
  limit?: number;
273
407
  before?: string;
274
408
  }): Promise<Row[]>;
275
- unreadTotal(): Promise<number>;
409
+ /** Unread across your conversations (for a badge), leaving out `excludeKinds` to match an inbox that does. */
410
+ unreadTotal(opts?: {
411
+ excludeKinds?: string[];
412
+ }): Promise<number>;
276
413
  conversation(id: string): Promise<ChatConversation | null>;
277
414
  /** A kind's rules (how many members, who posts, threads), for what the UI offers. */
278
415
  kind(kind: string): Promise<ChatKindRules | null>;
@@ -320,30 +457,69 @@ declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOp
320
457
  pins(conversationId: string): Promise<string[]>;
321
458
  save(messageId: string, on?: boolean, note?: string): Promise<void>;
322
459
  report(messageId: string, reason: "spam" | "harassment" | "safety" | "other", note?: string): Promise<string>;
460
+ /** A conversation's topics, in order (position, then key). */
461
+ topics(conversationId: string): Promise<ChatTopic[]>;
462
+ /** Adds a topic, or renames, reorders, closes (`closed: true`) or reopens it. Owners and admins. */
463
+ setTopic(conversationId: string, topic: string, input?: TopicInput): Promise<void>;
464
+ /** Schedules a message (a minute to a year ahead) and returns its id. Safe to retry with the same clientId. */
465
+ schedule(input: ScheduleInput): Promise<string>;
466
+ /** Your messages still waiting to go (and ones that failed when due), soonest first; in one conversation or all. */
467
+ scheduled(conversationId?: string): Promise<ScheduledMessage[]>;
468
+ cancelScheduled(id: string): Promise<void>;
469
+ /** The away message and hours of a conversation's members who set them (not observers). */
470
+ availabilityFor(conversationId: string): Promise<ChatAvailability[]>;
471
+ /** Your own away message and hours, or null when never set. */
472
+ myAvailability(): Promise<ChatAvailability | null>;
473
+ /** Replaces your away message and hours (a field left out is cleared). */
474
+ setAvailability(input: AvailabilityInput): Promise<void>;
475
+ /**
476
+ * Sends one text message into each of these conversations where you may post (a coach to
477
+ * each client's direct conversation), so replies come back privately. Text only. Safe to
478
+ * retry with the same clientId. Returns the ids of the messages sent.
479
+ */
480
+ broadcast(conversationIds: string[], clientId: string, body: string, opts?: {
481
+ silent?: boolean;
482
+ }): Promise<string[]>;
483
+ /** Messages (and transcripts) you can read that match, newest first: in one conversation or all of yours. Two characters at least. */
484
+ search(query: string, conversationId?: string | null, opts?: {
485
+ limit?: number;
486
+ }): Promise<ChatSearchHit[]>;
487
+ /**
488
+ * The previews the server has fetched for the links in these messages, by message id (only
489
+ * `ok` ones; links not fetched yet are left out). Read through link_previews_for, one call
490
+ * per message (up to 6 at once), which answers only for messages you can read: the preview
491
+ * table itself is closed, since a URL can carry a secret.
492
+ */
493
+ linkPreviewsFor(messageIds: string[]): Promise<Record<string, ChatLinkPreview[]>>;
323
494
  /**
324
495
  * Uploads a file into the conversation's folder for the signed-in person and
325
496
  * returns its path, ready to attach. Resumable for big files is the native
326
497
  * renderer's job (Supabase TUS); this is the plain upload.
498
+ *
499
+ * With `onProgress(sent, total)`, where XMLHttpRequest exists (browsers, React Native), the
500
+ * bytes go to a signed upload URL (`createSignedUploadUrl`, which checks the same storage
501
+ * policy as a plain upload) through XMLHttpRequest, which reports progress; elsewhere the
502
+ * plain upload runs and reports once, at the end. `signal` cancels it.
327
503
  */
328
504
  upload(conversationId: string, userId: string, file: Blob | ArrayBuffer, opts: {
329
505
  name: string;
330
506
  contentType: string;
331
507
  id?: string;
508
+ onProgress?: UploadProgress;
509
+ signal?: AbortSignal;
332
510
  }): Promise<string>;
333
511
  /** Signed links for files, cached until shortly before they expire. */
334
512
  fileUrls(paths: string[]): Promise<Record<string, string>>;
335
513
  /** Your inbox's live hints (a new message anywhere you are, a membership change). */
336
514
  subscribeInbox(userId: string, onSignal: (s: ChatSignal) => void): Subscription;
337
515
  /**
338
- * A conversation's live hints, plus who is typing or recording. `onReconnect`
339
- * runs whenever the channel (re)joins, the moment to fetch what was missed.
516
+ * A conversation's live hints, plus who is typing or recording. `onJoin` runs every
517
+ * time the channel joins, the first time included: the moment to fetch what arrived
518
+ * before the subscription was live. `onReconnect` (kept for older callers) runs on
519
+ * re-joins only.
340
520
  */
341
- subscribeConversation(conversationId: string, me: string, handlers: {
342
- onSignal: (s: ChatSignal) => void;
343
- onActivity?: (a: ChatActivity) => void;
344
- onReconnect?: () => void;
345
- }): ConversationChannel;
521
+ subscribeConversation(conversationId: string, me: string, handlers: ConversationHandlers): ConversationChannel;
346
522
  };
347
523
  type ChatClient = ReturnType<typeof createChatClient>;
348
524
 
349
- export { type AttachmentKind as A, type ChatClient as C, type MessagePage as M, type PendingMessage as P, type SendInput as S, type ChatConversation as a, type ChatMember as b, type ChatRole as c, type ChatKindRules as d, type ChatMessage as e, type ChatActivity as f, type ChatSignal as g, type ChatInboxItem as h, type ChatSupabase as i, type ChatAttachment as j, type AttachmentInput as k, type PendingFile as l, type AttachmentProvider as m, CHAT_BUCKET as n, CHAT_SCHEMA as o, type ChatClientOptions as p, ChatError as q, type ChatPreview as r, type ChatReaction as s, type ConversationChannel as t, type Subscription as u, conversationTopic as v, createChatClient as w, toAttachment as x, toMessage as y, userTopic as z };
525
+ export { type AttachmentInput as A, type PendingMessage as B, type ChatAvailability as C, type ScheduledMessage as D, type SendBytesOptions as E, type SendInput as F, type Subscription as G, type HostedAttachment as H, conversationTopic as I, createChatClient as J, sendBytes as K, toAttachment as L, type MessagePage as M, toAvailability as N, toLinkPreview as O, type PendingFile as P, toMessage as Q, toScheduled as R, type ScheduleInput as S, type TopicInput as T, type UploadProgress as U, toSearchHit as V, type Weekday as W, toTopic as X, userTopic as Y, type WorkingHours as a, type AttachmentKind as b, type AttachmentProvider as c, type AvailabilityInput as d, CHAT_BUCKET as e, CHAT_SCHEMA as f, type ChatActivity as g, type ChatAttachment as h, type ChatClient as i, type ChatClientOptions as j, type ChatConversation as k, ChatError as l, type ChatInboxItem as m, type ChatKindRules as n, type ChatLinkPreview as o, type ChatMember as p, type ChatMessage as q, type ChatPreview as r, type ChatReaction as s, type ChatRole as t, type ChatSearchHit as u, type ChatSignal as v, type ChatSupabase as w, type ChatTopic as x, type ConversationChannel as y, type ConversationHandlers as z };
package/dist/index.d.ts CHANGED
@@ -1,185 +1,100 @@
1
- import { A as AttachmentKind, e as ChatMessage, P as PendingMessage, f as ChatActivity, k as AttachmentInput, C as ChatClient, l as PendingFile, S as SendInput } from './client-BtdALFCx.js';
2
- export { m as AttachmentProvider, n as CHAT_BUCKET, o as CHAT_SCHEMA, j as ChatAttachment, p as ChatClientOptions, a as ChatConversation, q as ChatError, h as ChatInboxItem, d as ChatKindRules, b as ChatMember, r as ChatPreview, s as ChatReaction, c as ChatRole, g as ChatSignal, i as ChatSupabase, t as ConversationChannel, M as MessagePage, u as Subscription, v as conversationTopic, w as createChatClient, x as toAttachment, y as toMessage, z as userTopic } from './client-BtdALFCx.js';
1
+ import { W as Weekday, C as ChatAvailability, a as WorkingHours } from './client-DUIG1hum.js';
2
+ export { A as AttachmentInput, b as AttachmentKind, c as AttachmentProvider, d as AvailabilityInput, e as CHAT_BUCKET, f as CHAT_SCHEMA, g as ChatActivity, h as ChatAttachment, i as ChatClient, j as ChatClientOptions, k as ChatConversation, l as ChatError, m as ChatInboxItem, n as ChatKindRules, o as ChatLinkPreview, p as ChatMember, q as ChatMessage, r as ChatPreview, s as ChatReaction, t as ChatRole, u as ChatSearchHit, v as ChatSignal, w as ChatSupabase, x as ChatTopic, y as ConversationChannel, z as ConversationHandlers, H as HostedAttachment, M as MessagePage, P as PendingFile, B as PendingMessage, S as ScheduleInput, D as ScheduledMessage, E as SendBytesOptions, F as SendInput, G as Subscription, T as TopicInput, U as UploadProgress, I as conversationTopic, J as createChatClient, K as sendBytes, L as toAttachment, N as toAvailability, O as toLinkPreview, Q as toMessage, R as toScheduled, V as toSearchHit, X as toTopic, Y as userTopic } from './client-DUIG1hum.js';
3
+ export { A as ALLOWED_EXTENSIONS, C as CHAT_LIMITS, H as HostedUpload, a as HostedUploadOptions, b as HostedUploadRecord, c as HostedUploadStart, L as LocalFile, O as Outbox, d as OutboxOptions, e as OutboxStore, P as PLAYBACK_RATES, Q as QueuedMessage, S as StorageUpload, f as attachmentKindFor, g as createOutbox, h as fileExtension, i as formatDuration, j as isAllowedExtension, m as memoryOutboxStore, n as nextPlaybackRate, r as randomId, k as recordingLimitMs, l as releaseObjectUrl, w as waveformFromMetering, o as waveformPeaks } from './outbox-B9NAWQ58.js';
4
+ export { R as ReactionSummary, T as ThreadItem, a as activityLine, c as currentActivity, d as dayLabel, f as firstUnreadId, i as itemSender, b as itemTime, m as mergeThread, p as parseMentions, e as previewText, s as sameDay, g as startsDay, h as startsGroup, j as summarizeReactions, t as throttle, u as upsertMessages } from './thread-oPra7pkW.js';
3
5
  import '@supabase/supabase-js';
4
6
 
5
7
  /**
6
8
  * @feat:chat
7
- * Media rules shared by every platform: which files may be sent (the same list
8
- * migration 0001 checks), what kind an upload becomes, the default limits, and
9
- * the waveform a voice note carries so its bubble draws before the audio loads.
9
+ * Availability, the same on every platform: is someone inside their working hours (read in
10
+ * their own time zone), are they away, and the one line a thread shows about it ("Mark
11
+ * replies Mon–Fri, 9–5 (Eastern)", "Away until Monday: Back Monday"). Pure: `now` is passed
12
+ * in, and time zones come from Intl.
10
13
  */
11
14
 
15
+ declare const WEEKDAYS: readonly Weekday[];
16
+ /** "09:30" → 570; a bad value (anything but such a string) → null. "24:00" is allowed as the end of a day. */
17
+ declare function minutesOf(hhmm: unknown): number | null;
18
+ /** At most this many ranges a day, as the database allows. */
19
+ declare const MAX_RANGES_PER_DAY = 4;
12
20
  /**
13
- * A random UUID (v4): `crypto.randomUUID` where the platform has it (some React Native
14
- * runtimes don't), else `crypto.getRandomValues`, else Math.random as a last resort. Ids
15
- * only need to be unique per sender: the server checks (sender, client_id).
21
+ * Working hours as they may safely be read: only mon..sun, each an array of `["HH:MM", "HH:MM"]`
22
+ * string pairs (at most four a day); every other day, range or value is dropped. Null when
23
+ * nothing is left. Hours come from other people, so they are never trusted as typed.
16
24
  */
17
- declare function randomId(): string;
18
- /** Mirrors genesis_chat.allowed_extension. Never SVG or HTML, which a browser runs as a page. */
19
- declare const ALLOWED_EXTENSIONS: readonly ["png", "jpg", "jpeg", "webp", "gif", "heic", "heif", "m4a", "aac", "mp3", "ogg", "oga", "opus", "weba", "webm", "mp4", "mov", "pdf", "txt", "csv", "docx", "xlsx", "pptx"];
20
- /** The extension a file is stored under: from its content type, else its name. */
21
- declare function fileExtension(name: string, contentType?: string): string;
22
- declare function isAllowedExtension(ext: string): boolean;
23
- /** What an upload becomes in a message. */
24
- declare function attachmentKindFor(contentType: string, opts?: {
25
- recordedVoice?: boolean;
26
- }): AttachmentKind;
27
- /** Default limits (decision 5): an app may lower them, not raise them past the bucket's 100 MB. */
28
- declare const CHAT_LIMITS: {
29
- /** Voice notes and videos, for members. */
30
- readonly recordingMs: number;
31
- /** For owners, admins and observers (coaches, staff). */
32
- readonly staffRecordingMs: number;
33
- readonly fileBytes: number;
34
- readonly videoBytes: number;
35
- readonly attachmentsPerMessage: 20;
36
- readonly bodyChars: 10000;
25
+ declare function normalizeHours(raw: unknown): WorkingHours | null;
26
+ /** The weekday and minute of the day at `now` in `timeZone` (the device's zone when null or unknown). */
27
+ declare function localClock(now: Date, timeZone?: string | null): {
28
+ day: Weekday;
29
+ minutes: number;
37
30
  };
38
- /** The longest recording this role may make. */
39
- declare function recordingLimitMs(role: string | null | undefined): number;
40
31
  /**
41
- * Peaks for a waveform: `count` values 0–100 from raw samples (-1…1), the loudest
42
- * in each bucket, scaled so the loudest bucket is 100. Quiet recordings still show
43
- * a shape; silence shows a flat line.
32
+ * Is `now` inside these working hours, read in `timeZone`? No hours set means always.
33
+ * A range whose end is at or before its start runs overnight into the next day
34
+ * (`fri: [["22:00","02:00"]]` covers Friday 22:00 to Saturday 02:00).
44
35
  */
45
- declare function waveformPeaks(samples: ArrayLike<number>, count?: number): number[];
36
+ declare function isWithinWorkingHours(raw: WorkingHours | null | undefined, timeZone: string | null | undefined, now?: Date): boolean;
37
+ /** Away now: an away message with no end date, or an end date still ahead. */
38
+ declare function isAway(a: Pick<ChatAvailability, "awayMessage" | "awayUntil"> | null | undefined, now?: Date): boolean;
39
+ /** "Mon–Fri, 9–5", "Mon–Thu, 9–5; Fri, 9–1". Days with the same hours are grouped. Empty when none. */
40
+ declare function hoursSummary(raw: WorkingHours | null | undefined): string;
41
+ /** "Eastern" for America/New_York (the zone's generic name, less " Time"); the city when Intl has no name. */
42
+ declare function timeZoneLabel(timeZone: string | null | undefined, locale?: string): string;
43
+ /** "3:00 PM" today, "Monday" within the week, else "12 Mar". */
44
+ declare function untilLabel(iso: string, now?: Date, locale?: string, timeZone?: string | null): string;
45
+ interface AwayLineOptions {
46
+ now?: Date;
47
+ locale?: string;
48
+ /** Show the hours line even inside working hours (a profile, not a banner). */
49
+ always?: boolean;
50
+ }
46
51
  /**
47
- * Peaks from a level meter read while recording (expo-audio's `metering`, in dB,
48
- * about -160…0): resampled to `count` values 0–100.
52
+ * The line a thread shows about someone, or null when they are around:
53
+ * - away with a date: "Away until Monday: Back Monday" (the away message after the colon);
54
+ * - away with no date: "Mark is away: On a retreat";
55
+ * - outside working hours: "Mark replies Mon–Fri, 9–5 (Eastern)".
49
56
  */
50
- declare function waveformFromMetering(levelsDb: number[], count?: number): number[];
51
- /** 0:42, 1:05:09. */
52
- declare function formatDuration(ms: number | null | undefined): string;
53
- /** Playback speeds offered for voice and video (free for everyone, decision 5). */
54
- declare const PLAYBACK_RATES: readonly [1, 1.5, 2];
55
- declare function nextPlaybackRate(rate: number): number;
56
-
57
- /**
58
- * @feat:chat
59
- * Pure thread logic, the same on every platform: merging the server's messages
60
- * with ones still on their way, grouping a run of messages from one sender,
61
- * day dividers, the unread line, reactions by emoji, and the typing line.
62
- */
63
-
64
- type ThreadItem = {
65
- type: "message";
66
- message: ChatMessage;
67
- pending?: undefined;
68
- } | {
69
- type: "pending";
70
- pending: PendingMessage;
71
- message?: undefined;
72
- };
57
+ declare function awayLine(a: ChatAvailability | null | undefined, name: string, opts?: AwayLineOptions): string | null;
58
+ /** "Sam", "Sam and Lee", "Sam, Lee and 3 others": at most two names. */
59
+ declare function namesLine(names: string[]): string;
73
60
  /**
74
- * The server's messages (by seq), then what is still sending, oldest first. A
75
- * pending message the server already has (same clientId) is dropped, so a send
76
- * that landed never shows twice.
61
+ * The line for several people (a group's members): one person's full `awayLine`, or for more
62
+ * "Sam and Lee are away", "Sam, Lee and 2 others are outside their hours", or "… are away or
63
+ * outside their hours" when it's a mix. Null when everyone is around.
77
64
  */
78
- declare function mergeThread(server: ChatMessage[], pending: PendingMessage[]): ThreadItem[];
79
- /** Adds or replaces messages in a page, keeping it sorted and free of duplicates. */
80
- declare function upsertMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
81
- declare function itemSender(item: ThreadItem): string | null;
82
- declare function itemTime(item: ThreadItem): string;
83
- /** Runs of one sender within five minutes share a header (name, avatar). */
84
- declare function startsGroup(items: ThreadItem[], index: number, gapMs?: number): boolean;
85
- declare function sameDay(a: string, b: string): boolean;
86
- /** A day divider goes before this item. */
87
- declare function startsDay(items: ThreadItem[], index: number): boolean;
88
- /** "Today", "Yesterday", "Monday", or "12 Mar 2026". */
89
- declare function dayLabel(iso: string, now?: Date, locale?: string): string;
90
- /** The first message after what you had read, from someone else: where "New" goes. */
91
- declare function firstUnreadId(messages: ChatMessage[], lastReadSeq: number, me: string): string | null;
92
- interface ReactionSummary {
93
- emoji: string;
94
- count: number;
95
- mine: boolean;
96
- userIds: string[];
97
- }
98
- /** Reactions by emoji, most first, then in first-used order. */
99
- declare function summarizeReactions(message: ChatMessage, me: string): ReactionSummary[];
100
- /** Who is doing what now: activity older than six seconds has stopped. */
101
- declare function currentActivity(activity: ChatActivity[], now?: number, staleMs?: number): ChatActivity[];
102
- /** "Sam is typing…", "Sam and Lee are typing…", "Sam is recording a voice message…". */
103
- declare function activityLine(activity: ChatActivity[], nameOf: (userId: string) => string): string | null;
104
- /** Sends activity at most once every `everyMs`. */
105
- declare function throttle(fn: () => void, everyMs?: number): () => void;
106
- /** The ids @mentioned in markdown written as `@[Name](user:<uuid>)`. */
107
- declare function parseMentions(body: string): string[];
108
- /** A one-line preview: text, or what was attached. */
109
- declare function previewText(body: string, attachment: string | null | undefined, deleted?: boolean): string;
65
+ declare function awaySummary(people: {
66
+ availability: ChatAvailability | null | undefined;
67
+ name: string;
68
+ }[], opts?: AwayLineOptions): string | null;
110
69
 
111
70
  /**
112
71
  * @feat:chat
113
- * The send queue: a message shows at once as pending, its files upload, then it
114
- * is sent with the clientId made when it was written, so a retry (after a lost
115
- * connection, a closed app) never posts twice. Messages to one conversation go
116
- * in the order they were written. Storage is the platform's (memory by default;
117
- * localStorage on web, MMKV or SQLite on native), behind OutboxStore.
72
+ * Small pure helpers for migration 0002's features, shared by every platform: the links a
73
+ * message's text carries (the same rule the database uses to queue previews), https checks
74
+ * for preview images, search snippets split at their « » marks, scheduling quick picks, and
75
+ * topic keys.
118
76
  */
119
-
120
- /** A file still on the device, uploaded when its message is sent. */
121
- interface LocalFile {
122
- /** The bytes, or (native) a file URI the platform reads in `readFile`. */
123
- data: Blob | ArrayBuffer | string;
124
- name: string;
125
- contentType: string;
126
- /** A preview to show while it uploads (an object URL, a file URI). */
127
- previewUrl?: string;
128
- /** Known up front: a voice note's duration and waveform, a photo's size. */
129
- meta?: Partial<Omit<AttachmentInput, "path" | "kind">> & {
130
- kind?: AttachmentInput["kind"];
131
- };
77
+ /** The https links in a message's text that get a preview, at most three, as genesis_chat.message_links finds them. */
78
+ declare function extractLinks(body: string, max?: number): string[];
79
+ /** An absolute https URL with a host: the only kind a preview image may load from. */
80
+ declare function isHttpsUrl(url: string | null | undefined): url is string;
81
+ interface SnippetPart {
82
+ text: string;
83
+ match: boolean;
132
84
  }
133
- interface QueuedMessage extends Omit<PendingMessage, "files"> {
134
- files: LocalFile[];
135
- /** What each file is, kept with the saved copy (PendingMessage.files). */
136
- pendingFiles?: PendingFile[];
137
- /** Files lost on a relaunch (bytes that weren't persisted). */
138
- lostFiles?: number;
139
- attempts: number;
85
+ /** "a «word» here" → [{a }, {word, match}, { here}]. Unbalanced marks are kept as text. */
86
+ declare function snippetParts(snippet: string): SnippetPart[];
87
+ interface QuickPick {
88
+ label: string;
89
+ at: Date;
140
90
  }
141
- interface OutboxStore {
142
- load(): Promise<QueuedMessage[]>;
143
- save(queue: QueuedMessage[]): Promise<void>;
144
- }
145
- declare const memoryOutboxStore: () => OutboxStore;
146
- interface OutboxOptions {
147
- client: ChatClient;
148
- userId: string;
149
- store?: OutboxStore;
150
- /** Reads a native file URI into bytes for upload. */
151
- readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
152
- /** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. */
153
- upload?: (conversationId: string, file: LocalFile) => Promise<string>;
154
- /** Called with every change, for the UI. */
155
- onChange?: (queue: QueuedMessage[]) => void;
156
- /** Called when a message lands. */
157
- onSent?: (message: ChatMessage, clientId: string) => void;
158
- /** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */
159
- newId?: () => string;
160
- /**
161
- * Keeps a file held as bytes (a pasted image, a recording) between launches: write it
162
- * somewhere and return a URI `readFile` can read (native: the app's cache directory).
163
- * Without it, such a file isn't saved, and after a relaunch its message asks for it again.
164
- */
165
- persistFile?: (file: LocalFile) => Promise<string>;
166
- }
167
- declare function createOutbox(opts: OutboxOptions): {
168
- /** Queues a message; it shows at once and sends in order. Returns its clientId. */
169
- send(input: Omit<SendInput, "clientId"> & {
170
- clientId?: string;
171
- }, files?: LocalFile[]): Promise<string>;
172
- retry(clientId: string): Promise<void>;
173
- /** Gives a message whose files were lost on a relaunch its files again, and sends it. */
174
- reattach(clientId: string, files: LocalFile[]): Promise<void>;
175
- /** Retries every failed message (after the connection comes back). */
176
- retryAll(): Promise<void>;
177
- discard(clientId: string): Promise<void>;
178
- pending(conversationId?: string): PendingMessage[];
179
- /** A new client id, made the way this outbox makes them. */
180
- newId: typeof randomId;
181
- load: () => Promise<void>;
182
- };
183
- type Outbox = ReturnType<typeof createOutbox>;
91
+ /** "Tomorrow, 9:00 AM" and "Monday, 9:00 AM" (the next Monday after tomorrow; one pick when they're the same day). */
92
+ declare function scheduleQuickPicks(now?: Date, locale?: string): QuickPick[];
93
+ /** When a scheduled message goes: "Tomorrow, 9:00 AM", "Mon 12 Oct, 9:00 AM". */
94
+ declare function scheduleLabel(iso: string | Date, now?: Date, locale?: string): string;
95
+ /** The send time the database accepts: at least a minute and at most a year ahead. */
96
+ declare function isSchedulable(at: Date, now?: Date): boolean;
97
+ /** A topic's key from its title: "Week 1: Habits" → "week-1-habits" (at most 120 characters). */
98
+ declare function topicKey(title: string): string;
184
99
 
185
- export { ALLOWED_EXTENSIONS, AttachmentInput, AttachmentKind, CHAT_LIMITS, ChatActivity, ChatClient, ChatMessage, type LocalFile, type Outbox, type OutboxOptions, type OutboxStore, PLAYBACK_RATES, PendingFile, PendingMessage, type QueuedMessage, type ReactionSummary, SendInput, type ThreadItem, activityLine, attachmentKindFor, createOutbox, currentActivity, dayLabel, fileExtension, firstUnreadId, formatDuration, isAllowedExtension, itemSender, itemTime, memoryOutboxStore, mergeThread, nextPlaybackRate, parseMentions, previewText, randomId, recordingLimitMs, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages, waveformFromMetering, waveformPeaks };
100
+ export { type AwayLineOptions, ChatAvailability, MAX_RANGES_PER_DAY, type QuickPick, type SnippetPart, WEEKDAYS, Weekday, WorkingHours, awayLine, awaySummary, extractLinks, hoursSummary, isAway, isHttpsUrl, isSchedulable, isWithinWorkingHours, localClock, minutesOf, namesLine, normalizeHours, scheduleLabel, scheduleQuickPicks, snippetParts, timeZoneLabel, topicKey, untilLabel };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages } from './chunk-SAY24E6V.js';
2
- export { ALLOWED_EXTENSIONS, CHAT_BUCKET, CHAT_LIMITS, CHAT_SCHEMA, ChatError, PLAYBACK_RATES, attachmentKindFor, conversationTopic, createChatClient, fileExtension, formatDuration, isAllowedExtension, nextPlaybackRate, randomId, recordingLimitMs, toAttachment, toMessage, userTopic, waveformFromMetering, waveformPeaks } from './chunk-JQXZUEEL.js';
1
+ export { extractLinks, isHttpsUrl, isSchedulable, scheduleLabel, scheduleQuickPicks, snippetParts, topicKey } from './chunk-BJCE5O7V.js';
2
+ export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, releaseObjectUrl, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages } from './chunk-A4YJX7KT.js';
3
+ export { ALLOWED_EXTENSIONS, CHAT_BUCKET, CHAT_LIMITS, CHAT_SCHEMA, ChatError, MAX_RANGES_PER_DAY, PLAYBACK_RATES, WEEKDAYS, attachmentKindFor, awayLine, awaySummary, conversationTopic, createChatClient, fileExtension, formatDuration, hoursSummary, isAllowedExtension, isAway, isWithinWorkingHours, localClock, minutesOf, namesLine, nextPlaybackRate, normalizeHours, randomId, recordingLimitMs, sendBytes, timeZoneLabel, toAttachment, toAvailability, toLinkPreview, toMessage, toScheduled, toSearchHit, toTopic, untilLabel, userTopic, waveformFromMetering, waveformPeaks } from './chunk-GBJ3YI4E.js';
3
4
  //# sourceMappingURL=index.js.map
4
5
  //# sourceMappingURL=index.js.map