@marktiderman/genesis-chat 0.1.0 → 0.2.0

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 (162) hide show
  1. package/README.md +304 -13
  2. package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
  3. package/dist/chunk-5D5A7I4Q.js.map +1 -0
  4. package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
  5. package/dist/chunk-7S6HYNWW.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-PZTOY2LY.js +576 -0
  9. package/dist/chunk-PZTOY2LY.js.map +1 -0
  10. package/dist/chunk-VEPZROQB.js +187 -0
  11. package/dist/chunk-VEPZROQB.js.map +1 -0
  12. package/dist/{client-BtdALFCx.d.ts → client-DMa2j7kz.d.ts} +152 -11
  13. package/dist/index.d.ts +78 -163
  14. package/dist/index.js +3 -2
  15. package/dist/native.d.ts +1910 -1
  16. package/dist/native.js +4900 -0
  17. package/dist/native.js.map +1 -1
  18. package/dist/outbox-B0y3phE0.d.ts +151 -0
  19. package/dist/push.d.ts +206 -0
  20. package/dist/push.js +3 -0
  21. package/dist/push.js.map +1 -0
  22. package/dist/react.d.ts +149 -7
  23. package/dist/react.js +3 -3
  24. package/dist/server.d.ts +510 -21
  25. package/dist/server.js +863 -21
  26. package/dist/server.js.map +1 -1
  27. package/dist/thread-DWXHKPdw.d.ts +57 -0
  28. package/dist/web.d.ts +313 -10
  29. package/dist/web.js +1731 -60
  30. package/dist/web.js.map +1 -1
  31. package/migrations/0002_genesis_chat_coaching.sql +934 -0
  32. package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
  33. package/package.json +74 -6
  34. package/src/__tests__/coaching-core.test.ts +332 -0
  35. package/src/__tests__/coaching-react.test.tsx +248 -0
  36. package/src/__tests__/coaching-web.test.tsx +449 -0
  37. package/src/__tests__/core.test.ts +91 -0
  38. package/src/__tests__/outbox.test.ts +122 -0
  39. package/src/__tests__/realtime.test.ts +70 -0
  40. package/src/__tests__/review-fixes-core.test.ts +193 -0
  41. package/src/__tests__/review-fixes-web.test.tsx +402 -0
  42. package/src/__tests__/server-effects.test.ts +158 -0
  43. package/src/__tests__/server-fakes.ts +90 -0
  44. package/src/__tests__/server-integrations.test.ts +247 -0
  45. package/src/__tests__/server-providers.test.ts +246 -0
  46. package/src/__tests__/server-runner.test.ts +277 -0
  47. package/src/__tests__/server-safety.test.ts +290 -0
  48. package/src/__tests__/web-markdown.test.tsx +88 -0
  49. package/src/__tests__/web.test.tsx +333 -0
  50. package/src/core/availability.ts +223 -0
  51. package/src/core/client.ts +757 -0
  52. package/src/core/extras.ts +106 -0
  53. package/src/core/media.ts +144 -0
  54. package/src/core/outbox.ts +429 -0
  55. package/src/core/thread.ts +171 -0
  56. package/src/core/types.ts +311 -0
  57. package/src/index.ts +13 -0
  58. package/src/native/__tests__/auto-retry.test.tsx +128 -0
  59. package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
  60. package/src/native/__tests__/composer-state.test.ts +188 -0
  61. package/src/native/__tests__/composer.test.tsx +133 -0
  62. package/src/native/__tests__/emoji.test.tsx +238 -0
  63. package/src/native/__tests__/inbox-screen.test.tsx +48 -0
  64. package/src/native/__tests__/message-bubble.test.tsx +127 -0
  65. package/src/native/__tests__/organise-logic.test.ts +164 -0
  66. package/src/native/__tests__/organise-screens.test.tsx +344 -0
  67. package/src/native/__tests__/photo-viewer.test.tsx +226 -0
  68. package/src/native/__tests__/push-payload.test.ts +66 -0
  69. package/src/native/__tests__/push.test.ts +209 -0
  70. package/src/native/__tests__/recorder-machine.test.ts +96 -0
  71. package/src/native/__tests__/rows-actions.test.ts +76 -0
  72. package/src/native/__tests__/thread-layout.test.ts +41 -0
  73. package/src/native/__tests__/thread-screen.test.tsx +116 -0
  74. package/src/native/__tests__/tus.test.ts +154 -0
  75. package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
  76. package/src/native/__tests__/video-media.test.tsx +421 -0
  77. package/src/native/__tests__/video-recorder-state.test.ts +87 -0
  78. package/src/native/__tests__/video-upload.test.ts +227 -0
  79. package/src/native/access-error.tsx +31 -0
  80. package/src/native/actions.ts +42 -0
  81. package/src/native/attachment-cards.tsx +116 -0
  82. package/src/native/auto-retry.ts +69 -0
  83. package/src/native/availability.tsx +376 -0
  84. package/src/native/broadcast-sheet.tsx +193 -0
  85. package/src/native/composer-state.ts +302 -0
  86. package/src/native/composer.tsx +453 -0
  87. package/src/native/context.tsx +70 -0
  88. package/src/native/copy.ts +296 -0
  89. package/src/native/emoji-data.ts +21 -0
  90. package/src/native/emoji-picker.tsx +229 -0
  91. package/src/native/emoji.ts +118 -0
  92. package/src/native/icon-button.tsx +40 -0
  93. package/src/native/inbox-screen.tsx +116 -0
  94. package/src/native/index.ts +184 -0
  95. package/src/native/link-preview.tsx +104 -0
  96. package/src/native/media-colors.ts +15 -0
  97. package/src/native/media.tsx +187 -0
  98. package/src/native/message-actions-sheet.tsx +121 -0
  99. package/src/native/message-bubble.tsx +374 -0
  100. package/src/native/moderation.tsx +42 -0
  101. package/src/native/modules.ts +275 -0
  102. package/src/native/organise-copy.ts +195 -0
  103. package/src/native/photo-viewer.tsx +531 -0
  104. package/src/native/push.ts +375 -0
  105. package/src/native/recorder-machine.ts +176 -0
  106. package/src/native/rows.ts +81 -0
  107. package/src/native/schedule-sheet.tsx +314 -0
  108. package/src/native/search-screen.tsx +168 -0
  109. package/src/native/stack-options.ts +36 -0
  110. package/src/native/start-sheet.tsx +87 -0
  111. package/src/native/thread-screen.tsx +620 -0
  112. package/src/native/topics.tsx +179 -0
  113. package/src/native/tus.ts +282 -0
  114. package/src/native/use-composer.ts +71 -0
  115. package/src/native/use-jump.ts +91 -0
  116. package/src/native/use-voice-recorder.ts +234 -0
  117. package/src/native/video-player.tsx +370 -0
  118. package/src/native/video-recorder-state.ts +140 -0
  119. package/src/native/video-recorder.tsx +271 -0
  120. package/src/native/video-upload-tray.tsx +77 -0
  121. package/src/native/video-upload.ts +432 -0
  122. package/src/native/voice-player.tsx +283 -0
  123. package/src/react/index.tsx +810 -0
  124. package/src/server/assistant.ts +235 -0
  125. package/src/server/crypto.ts +86 -0
  126. package/src/server/expo.ts +119 -0
  127. package/src/server/handler.ts +32 -0
  128. package/src/server/index.ts +57 -0
  129. package/src/server/moderation.ts +155 -0
  130. package/src/server/mux.ts +290 -0
  131. package/src/server/notifications.ts +130 -0
  132. package/src/server/runner.ts +468 -0
  133. package/src/server/transcribers.ts +111 -0
  134. package/src/server/types.ts +171 -0
  135. package/src/server/unfurl.ts +480 -0
  136. package/src/server/upload.ts +158 -0
  137. package/src/web/attachments.tsx +379 -0
  138. package/src/web/availability.tsx +299 -0
  139. package/src/web/broadcast.tsx +166 -0
  140. package/src/web/bubble.tsx +563 -0
  141. package/src/web/cards.tsx +136 -0
  142. package/src/web/composer.tsx +571 -0
  143. package/src/web/emoji-picker.tsx +84 -0
  144. package/src/web/error-boundary.tsx +33 -0
  145. package/src/web/format.ts +55 -0
  146. package/src/web/inbox.tsx +195 -0
  147. package/src/web/index.ts +75 -0
  148. package/src/web/markdown.tsx +245 -0
  149. package/src/web/memory-client.ts +706 -0
  150. package/src/web/panel.tsx +135 -0
  151. package/src/web/schedule.tsx +192 -0
  152. package/src/web/search.tsx +129 -0
  153. package/src/web/thread-context.ts +64 -0
  154. package/src/web/thread.tsx +802 -0
  155. package/src/web/topics.tsx +160 -0
  156. package/src/web/use-voice-recorder.ts +264 -0
  157. package/src/web/video.tsx +522 -0
  158. package/src/web/voice-note.tsx +197 -0
  159. package/dist/chunk-27UTR5FW.js +0 -322
  160. package/dist/chunk-27UTR5FW.js.map +0 -1
  161. package/dist/chunk-JQXZUEEL.js.map +0 -1
  162. 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). */
@@ -215,10 +222,87 @@ interface ChatActivity {
215
222
  activity: "typing" | "recording-voice" | "recording-video";
216
223
  at: number;
217
224
  }
225
+ /** A named sub-thread of a conversation (a cohort's lesson threads). Messages join one through `topic`. */
226
+ interface ChatTopic {
227
+ conversationId: string;
228
+ /** The key messages carry in `topic`. */
229
+ topic: string;
230
+ title: string;
231
+ position: number;
232
+ /** Set when closed: only owners, admins and observers post in it then. */
233
+ closedAt: string | null;
234
+ createdBy: string | null;
235
+ createdAt: string;
236
+ }
237
+ /** What `setTopic` changes; anything left out keeps its value (a new topic's title is its key). */
238
+ interface TopicInput {
239
+ title?: string;
240
+ position?: number;
241
+ closed?: boolean;
242
+ }
243
+ /** A message to send later, as its sender. Only its sender reads it. */
244
+ interface ScheduledMessage {
245
+ id: string;
246
+ conversationId: string;
247
+ clientId: string;
248
+ body: string;
249
+ attachments: AttachmentInput[];
250
+ replyToId: string | null;
251
+ topic: string | null;
252
+ mentions: string[];
253
+ silent: boolean;
254
+ sendAt: string;
255
+ sentMessageId: string | null;
256
+ cancelledAt: string | null;
257
+ /** Why it couldn't be sent when due (the sender left the conversation, say). */
258
+ error: string | null;
259
+ createdAt: string;
260
+ status: "scheduled" | "sent" | "cancelled" | "failed";
261
+ }
262
+ /** Between a minute and a year ahead. Files in `attachments` are uploaded first, as for `send`. */
263
+ interface ScheduleInput extends Omit<SendInput, "threadRootId" | "visibility"> {
264
+ sendAt: string | Date;
265
+ }
266
+ type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
267
+ /** 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. */
268
+ type WorkingHours = Partial<Record<Weekday, [string, string][]>>;
269
+ /** A person's away message and working hours. */
270
+ interface ChatAvailability {
271
+ userId: string;
272
+ awayMessage: string | null;
273
+ /** Away until then; with an away message and no date, away until they clear it. */
274
+ awayUntil: string | null;
275
+ /** An IANA zone (`America/New_York`); hours are read in it. */
276
+ timeZone: string | null;
277
+ hours: WorkingHours | null;
278
+ }
279
+ type AvailabilityInput = Partial<Omit<ChatAvailability, "userId">>;
280
+ /** One search result. `snippet` marks the matched words with « and » (the text's own « and » arrive as ‹ and ›, so marks are always real). */
281
+ interface ChatSearchHit {
282
+ messageId: string;
283
+ conversationId: string;
284
+ seq: number;
285
+ senderId: string | null;
286
+ createdAt: string;
287
+ snippet: string;
288
+ /** Found in a voice note's or video's transcript, not the text. */
289
+ inTranscript: boolean;
290
+ }
291
+ /** Open Graph data for a link (fetched by the server's runner). Plain text: never render it as HTML. */
292
+ interface ChatLinkPreview {
293
+ url: string;
294
+ status: "ok" | "failed" | "blocked";
295
+ title: string | null;
296
+ description: string | null;
297
+ /** Always https when present. */
298
+ imageUrl: string | null;
299
+ siteName: string | null;
300
+ fetchedAt: string;
301
+ }
218
302
 
219
303
  /**
220
304
  * @feat:chat
221
- * The chat client: the app's own Supabase (migration 0001, the genesis_chat
305
+ * The chat client: the app's own Supabase (migrations 0001 and 0002, the genesis_chat
222
306
  * schema) under row level security. Reads are table selects; every write is a
223
307
  * genesis_chat function. Live updates are hints on private Realtime topics,
224
308
  * after which the client fetches again, so nothing is trusted from the wire.
@@ -239,10 +323,21 @@ declare class ChatError extends Error {
239
323
  get rateLimited(): boolean;
240
324
  /** Worth trying again: the network, not the database, said no. */
241
325
  get transient(): boolean;
326
+ /**
327
+ * Sending it again won't help: the database refused what was sent (not allowed 42501, bad
328
+ * data 22xxx such as 22023, a conflict 23xxx such as 23505, another refusal 42xxx, or a
329
+ * function's own `raise` P0001). A person may still retry by hand; nothing retries it alone.
330
+ */
331
+ get permanent(): boolean;
242
332
  }
243
333
  type Row = Record<string, unknown>;
244
334
  declare function toAttachment(r: Row): ChatAttachment;
245
335
  declare function toMessage(r: Row): ChatMessage;
336
+ declare function toTopic(r: Row): ChatTopic;
337
+ declare function toScheduled(r: Row): ScheduledMessage;
338
+ declare function toAvailability(r: Row): ChatAvailability;
339
+ declare function toSearchHit(r: Row): ChatSearchHit;
340
+ declare function toLinkPreview(r: Row): ChatLinkPreview;
246
341
  interface MessagePage {
247
342
  messages: ChatMessage[];
248
343
  /** More, older messages exist before the first one. */
@@ -255,14 +350,25 @@ interface ChatClientOptions {
255
350
  interface Subscription {
256
351
  unsubscribe(): void;
257
352
  }
353
+ interface ConversationHandlers {
354
+ onSignal: (s: ChatSignal) => void;
355
+ onActivity?: (a: ChatActivity) => void;
356
+ /** Every join, the first included: catch up on what arrived before the channel was live. */
357
+ onJoin?: () => void;
358
+ /** Re-joins only. Prefer `onJoin`. */
359
+ onReconnect?: () => void;
360
+ }
258
361
  interface ConversationChannel extends Subscription {
259
362
  /** Tell the others you are typing or recording (throttle to once every 3 seconds). */
260
363
  signal(activity: ChatActivity["activity"]): void;
261
364
  }
262
365
  declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOptions): {
263
366
  supabase: ChatSupabase;
367
+ /** Your conversations: one `kind`, several `kinds`, or all but `excludeKinds` (e.g. Messages without `support`). */
264
368
  inbox(opts?: {
265
369
  kind?: string;
370
+ kinds?: string[];
371
+ excludeKinds?: string[];
266
372
  archived?: boolean;
267
373
  limit?: number;
268
374
  before?: string;
@@ -272,7 +378,10 @@ declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOp
272
378
  limit?: number;
273
379
  before?: string;
274
380
  }): Promise<Row[]>;
275
- unreadTotal(): Promise<number>;
381
+ /** Unread across your conversations (for a badge), leaving out `excludeKinds` to match an inbox that does. */
382
+ unreadTotal(opts?: {
383
+ excludeKinds?: string[];
384
+ }): Promise<number>;
276
385
  conversation(id: string): Promise<ChatConversation | null>;
277
386
  /** A kind's rules (how many members, who posts, threads), for what the UI offers. */
278
387
  kind(kind: string): Promise<ChatKindRules | null>;
@@ -320,6 +429,40 @@ declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOp
320
429
  pins(conversationId: string): Promise<string[]>;
321
430
  save(messageId: string, on?: boolean, note?: string): Promise<void>;
322
431
  report(messageId: string, reason: "spam" | "harassment" | "safety" | "other", note?: string): Promise<string>;
432
+ /** A conversation's topics, in order (position, then key). */
433
+ topics(conversationId: string): Promise<ChatTopic[]>;
434
+ /** Adds a topic, or renames, reorders, closes (`closed: true`) or reopens it. Owners and admins. */
435
+ setTopic(conversationId: string, topic: string, input?: TopicInput): Promise<void>;
436
+ /** Schedules a message (a minute to a year ahead) and returns its id. Safe to retry with the same clientId. */
437
+ schedule(input: ScheduleInput): Promise<string>;
438
+ /** Your messages still waiting to go (and ones that failed when due), soonest first; in one conversation or all. */
439
+ scheduled(conversationId?: string): Promise<ScheduledMessage[]>;
440
+ cancelScheduled(id: string): Promise<void>;
441
+ /** The away message and hours of a conversation's members who set them (not observers). */
442
+ availabilityFor(conversationId: string): Promise<ChatAvailability[]>;
443
+ /** Your own away message and hours, or null when never set. */
444
+ myAvailability(): Promise<ChatAvailability | null>;
445
+ /** Replaces your away message and hours (a field left out is cleared). */
446
+ setAvailability(input: AvailabilityInput): Promise<void>;
447
+ /**
448
+ * Sends one text message into each of these conversations where you may post (a coach to
449
+ * each client's direct conversation), so replies come back privately. Text only. Safe to
450
+ * retry with the same clientId. Returns the ids of the messages sent.
451
+ */
452
+ broadcast(conversationIds: string[], clientId: string, body: string, opts?: {
453
+ silent?: boolean;
454
+ }): Promise<string[]>;
455
+ /** Messages (and transcripts) you can read that match, newest first: in one conversation or all of yours. Two characters at least. */
456
+ search(query: string, conversationId?: string | null, opts?: {
457
+ limit?: number;
458
+ }): Promise<ChatSearchHit[]>;
459
+ /**
460
+ * The previews the server has fetched for the links in these messages, by message id (only
461
+ * `ok` ones; links not fetched yet are left out). Read through link_previews_for, one call
462
+ * per message (up to 6 at once), which answers only for messages you can read: the preview
463
+ * table itself is closed, since a URL can carry a secret.
464
+ */
465
+ linkPreviewsFor(messageIds: string[]): Promise<Record<string, ChatLinkPreview[]>>;
323
466
  /**
324
467
  * Uploads a file into the conversation's folder for the signed-in person and
325
468
  * returns its path, ready to attach. Resumable for big files is the native
@@ -335,15 +478,13 @@ declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOp
335
478
  /** Your inbox's live hints (a new message anywhere you are, a membership change). */
336
479
  subscribeInbox(userId: string, onSignal: (s: ChatSignal) => void): Subscription;
337
480
  /**
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.
481
+ * A conversation's live hints, plus who is typing or recording. `onJoin` runs every
482
+ * time the channel joins, the first time included: the moment to fetch what arrived
483
+ * before the subscription was live. `onReconnect` (kept for older callers) runs on
484
+ * re-joins only.
340
485
  */
341
- subscribeConversation(conversationId: string, me: string, handlers: {
342
- onSignal: (s: ChatSignal) => void;
343
- onActivity?: (a: ChatActivity) => void;
344
- onReconnect?: () => void;
345
- }): ConversationChannel;
486
+ subscribeConversation(conversationId: string, me: string, handlers: ConversationHandlers): ConversationChannel;
346
487
  };
347
488
  type ChatClient = ReturnType<typeof createChatClient>;
348
489
 
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 };
490
+ export { type AttachmentInput as A, type PendingMessage as B, type ChatAvailability as C, type ScheduledMessage as D, type SendInput as E, type Subscription as F, conversationTopic as G, createChatClient as H, toAttachment as I, toAvailability as J, toLinkPreview as K, toMessage as L, type MessagePage as M, toScheduled as N, toSearchHit as O, type PendingFile as P, toTopic as Q, userTopic as R, type ScheduleInput as S, type TopicInput as T, type Weekday as W, 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-DMa2j7kz.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, M as MessagePage, P as PendingFile, B as PendingMessage, S as ScheduleInput, D as ScheduledMessage, E as SendInput, F as Subscription, T as TopicInput, G as conversationTopic, H as createChatClient, I as toAttachment, J as toAvailability, K as toLinkPreview, L as toMessage, N as toScheduled, O as toSearchHit, Q as toTopic, R as userTopic } from './client-DMa2j7kz.js';
3
+ export { A as ALLOWED_EXTENSIONS, C as CHAT_LIMITS, L as LocalFile, O as Outbox, a as OutboxOptions, b as OutboxStore, P as PLAYBACK_RATES, Q as QueuedMessage, c as attachmentKindFor, d as createOutbox, f as fileExtension, e as formatDuration, i as isAllowedExtension, m as memoryOutboxStore, n as nextPlaybackRate, r as randomId, g as recordingLimitMs, h as releaseObjectUrl, w as waveformFromMetering, j as waveformPeaks } from './outbox-B0y3phE0.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-DWXHKPdw.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-5D5A7I4Q.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, timeZoneLabel, toAttachment, toAvailability, toLinkPreview, toMessage, toScheduled, toSearchHit, toTopic, untilLabel, userTopic, waveformFromMetering, waveformPeaks } from './chunk-7S6HYNWW.js';
3
4
  //# sourceMappingURL=index.js.map
4
5
  //# sourceMappingURL=index.js.map