@marktiderman/genesis-chat 0.1.1 → 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
@@ -0,0 +1,57 @@
1
+ import { q as ChatMessage, B as PendingMessage, g as ChatActivity } from './client-DMa2j7kz.js';
2
+
3
+ /**
4
+ * @feat:chat
5
+ * Pure thread logic, the same on every platform: merging the server's messages
6
+ * with ones still on their way, grouping a run of messages from one sender,
7
+ * day dividers, the unread line, reactions by emoji, and the typing line.
8
+ */
9
+
10
+ type ThreadItem = {
11
+ type: "message";
12
+ message: ChatMessage;
13
+ pending?: undefined;
14
+ } | {
15
+ type: "pending";
16
+ pending: PendingMessage;
17
+ message?: undefined;
18
+ };
19
+ /**
20
+ * The server's messages (by seq), then what is still sending, oldest first. A
21
+ * pending message the server already has (same clientId) is dropped, so a send
22
+ * that landed never shows twice.
23
+ */
24
+ declare function mergeThread(server: ChatMessage[], pending: PendingMessage[]): ThreadItem[];
25
+ /** Adds or replaces messages in a page, keeping it sorted and free of duplicates. */
26
+ declare function upsertMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
27
+ declare function itemSender(item: ThreadItem): string | null;
28
+ declare function itemTime(item: ThreadItem): string;
29
+ /** Runs of one sender within five minutes share a header (name, avatar). */
30
+ declare function startsGroup(items: ThreadItem[], index: number, gapMs?: number): boolean;
31
+ declare function sameDay(a: string, b: string): boolean;
32
+ /** A day divider goes before this item. */
33
+ declare function startsDay(items: ThreadItem[], index: number): boolean;
34
+ /** "Today", "Yesterday", "Monday", or "12 Mar 2026". */
35
+ declare function dayLabel(iso: string, now?: Date, locale?: string): string;
36
+ /** The first message after what you had read, from someone else: where "New" goes. */
37
+ declare function firstUnreadId(messages: ChatMessage[], lastReadSeq: number, me: string): string | null;
38
+ interface ReactionSummary {
39
+ emoji: string;
40
+ count: number;
41
+ mine: boolean;
42
+ userIds: string[];
43
+ }
44
+ /** Reactions by emoji, most first, then in first-used order. */
45
+ declare function summarizeReactions(message: ChatMessage, me: string): ReactionSummary[];
46
+ /** Who is doing what now: activity older than six seconds has stopped. */
47
+ declare function currentActivity(activity: ChatActivity[], now?: number, staleMs?: number): ChatActivity[];
48
+ /** "Sam is typing…", "Sam and Lee are typing…", "Sam is recording a voice message…". */
49
+ declare function activityLine(activity: ChatActivity[], nameOf: (userId: string) => string): string | null;
50
+ /** Sends activity at most once every `everyMs`. */
51
+ declare function throttle(fn: () => void, everyMs?: number): () => void;
52
+ /** The ids @mentioned in markdown written as `@[Name](user:<uuid>)`. */
53
+ declare function parseMentions(body: string): string[];
54
+ /** A one-line preview: text, or what was attached. */
55
+ declare function previewText(body: string, attachment: string | null | undefined, deleted?: boolean): string;
56
+
57
+ export { type ReactionSummary as R, type ThreadItem as T, activityLine as a, itemTime as b, currentActivity as c, dayLabel as d, previewText as e, firstUnreadId as f, startsDay as g, startsGroup as h, itemSender as i, summarizeReactions as j, mergeThread as m, parseMentions as p, sameDay as s, throttle as t, upsertMessages as u };
package/dist/web.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as react from 'react';
2
- import { ComponentType, ReactNode, Ref } from 'react';
2
+ import { ComponentType, ReactNode, Ref, Component, ErrorInfo } from 'react';
3
3
  import { SlotOverride } from '@marktiderman/genesis-core/slots';
4
- import { e as ChatMessage, h as ChatInboxItem, j as ChatAttachment, a as ChatConversation, b as ChatMember, c as ChatRole, C as ChatClient, f as ChatActivity } from './client-BtdALFCx.js';
5
- import { ConversationState } from './react.js';
6
- import { ThreadItem, LocalFile } from './index.js';
4
+ import { q as ChatMessage, m as ChatInboxItem, o as ChatLinkPreview$1, h as ChatAttachment, k as ChatConversation, p as ChatMember, t as ChatRole, i as ChatClient, g as ChatActivity, C as ChatAvailability, x as ChatTopic, T as TopicInput, u as ChatSearchHit, E as SendInput, A as AttachmentInput } from './client-DMa2j7kz.js';
5
+ import { ChatAttachmentKind, ConversationState, ChatVideoUpload } from './react.js';
6
+ import { T as ThreadItem } from './thread-DWXHKPdw.js';
7
+ import { L as LocalFile } from './outbox-B0y3phE0.js';
7
8
  import '@supabase/supabase-js';
8
9
  import '@tanstack/query-core';
9
10
 
@@ -49,6 +50,46 @@ interface ChatInboxProps {
49
50
  declare function inboxTitle(item: ChatInboxItem, resolveUser: ResolveUser, fallback?: (item: ChatInboxItem) => string): string;
50
51
  declare function ChatInbox({ resolveUser, onSelect, selectedId, kind, archived, fallbackTitle, slots, className, ...rest }: ChatInboxProps): react.JSX.Element;
51
52
 
53
+ /**
54
+ * Whether a link preview shows its image. The image lives on the linked site, so loading it
55
+ * straight from there tells that site each reader's IP address and when they read the message
56
+ * (a tracking pixel the sender controls). So:
57
+ * - `"off"` (the default): no image;
58
+ * - `"proxy"`: through your own image proxy, `proxyUrl(imageUrl)`; no image without it;
59
+ * - `"direct"`: from the linked site (only if you accept that).
60
+ */
61
+ type LinkPreviewImages = "off" | "proxy" | "direct";
62
+ /** The image a preview may show under this setting, or null. Only https image URLs are ever considered. */
63
+ declare function linkPreviewImageSrc(imageUrl: string | null | undefined, images?: LinkPreviewImages, proxyUrl?: (url: string) => string | null | undefined): string | null;
64
+ interface ChatLinkPreviewProps {
65
+ preview: ChatLinkPreview$1;
66
+ /** Whether to show its image, and how; `"off"` by default (see LinkPreviewImages). */
67
+ images?: LinkPreviewImages;
68
+ /** Your image proxy, for `images: "proxy"`: the URL to load instead of the linked site's. */
69
+ proxyUrl?: (url: string) => string | null | undefined;
70
+ tone?: "default" | "primary";
71
+ className?: string;
72
+ }
73
+ declare function ChatLinkPreview({ preview, images, proxyUrl, tone, className }: ChatLinkPreviewProps): react.JSX.Element | null;
74
+ interface ChatAttachmentCardProps {
75
+ title: string;
76
+ subtitle?: string | null;
77
+ /** An icon component (lucide); a clipboard by default. */
78
+ icon?: ComponentType<{
79
+ className?: string;
80
+ }>;
81
+ /** One button: a link (`href`, safe schemes only) or a handler. */
82
+ action?: {
83
+ label: string;
84
+ href?: string;
85
+ onPress?: () => void;
86
+ } | null;
87
+ tone?: "default" | "primary";
88
+ className?: string;
89
+ }
90
+ /** A card for an app's own attachment kind: an icon, a title, a subtitle and one action. */
91
+ declare function ChatAttachmentCard({ title, subtitle, icon: Icon, action, tone, className }: ChatAttachmentCardProps): react.JSX.Element;
92
+
52
93
  /** One attachment as the renderers draw it, sent or still uploading. */
53
94
  interface AttachmentView {
54
95
  key: string;
@@ -95,6 +136,7 @@ interface LightboxProps {
95
136
  }
96
137
  /** A photo full size, with the previous and next ones on the arrow keys. */
97
138
  declare function Lightbox({ images, index, onIndexChange }: LightboxProps): react.JSX.Element;
139
+ /** A video: a hosted one (Mux) streams once ready and says "Processing…" until then; a file plays from storage. */
98
140
  declare function VideoAttachment({ view }: {
99
141
  view: AttachmentView;
100
142
  }): react.JSX.Element;
@@ -109,9 +151,11 @@ interface MessageAttachmentsProps {
109
151
  tone: "default" | "primary";
110
152
  /** For kinds the package doesn't draw; a generic card by default. */
111
153
  slot?: ComponentType<ChatAttachmentSlotProps> | null;
154
+ /** Cards for registered app kinds (ChatProvider `attachmentKinds`), drawn before the slot. */
155
+ kinds?: Record<string, ChatAttachmentKind>;
112
156
  }
113
157
  /** Every attachment of one message, photos grouped into one grid. */
114
- declare function MessageAttachments({ views, message, mine, tone, slot }: MessageAttachmentsProps): react.JSX.Element | null;
158
+ declare function MessageAttachments({ views, message, mine, tone, slot, kinds }: MessageAttachmentsProps): react.JSX.Element | null;
115
159
 
116
160
  interface ChatThreadHeaderProps {
117
161
  conversationId: string;
@@ -129,6 +173,8 @@ interface ChatThreadHeaderProps {
129
173
  resolveUser: ResolveUser;
130
174
  onBack?: () => void;
131
175
  backClassName?: string;
176
+ /** Opens search in this conversation (absent when search is off). */
177
+ onSearch?: () => void;
132
178
  }
133
179
  interface ChatThreadEmptyProps {
134
180
  conversationId: string;
@@ -168,10 +214,29 @@ interface ChatThreadProps {
168
214
  /** The quick reactions; 👍 ❤️ 😂 😮 😢 🙏 by default. */
169
215
  quickReactions?: readonly string[];
170
216
  placeholder?: string;
217
+ /** Show the topic bar even before the first topic exists (so an owner can add one). */
218
+ showTopics?: boolean;
219
+ /** Scroll to this message once it's loaded (a search result across conversations); `seq` lets earlier pages load to reach it. */
220
+ jumpTo?: {
221
+ messageId: string;
222
+ seq?: number;
223
+ } | null;
224
+ /** A search button in the header. Default true. */
225
+ search?: boolean;
226
+ /** Scheduled send in the composer. Default true. */
227
+ scheduling?: boolean;
228
+ /** The away banner. Default true. */
229
+ availability?: boolean;
230
+ /** Where the emoji picker loads its data from (self-hosted emojibase-data). Without it there is no full picker; the quick reactions stay. */
231
+ emojibaseUrl?: string;
232
+ /** Link preview images: `"off"` by default, `"proxy"` through `linkPreviewProxyUrl`, or `"direct"` from the linked site (which then sees readers' IPs). */
233
+ linkPreviewImages?: LinkPreviewImages;
234
+ /** Your image proxy for `linkPreviewImages: "proxy"`: the URL to load for an og:image. */
235
+ linkPreviewProxyUrl?: (url: string) => string | null | undefined;
171
236
  slots?: ChatThreadSlots;
172
237
  className?: string;
173
238
  }
174
- declare function ChatThread({ conversationId, resolveUser, title: titleProp, onBack, backClassName, threadRootId, topic, onOpenThread, quickReactions, placeholder, slots, className, }: ChatThreadProps): react.JSX.Element;
239
+ declare function ChatThread({ conversationId, resolveUser, title: titleProp, onBack, backClassName, threadRootId, topic: topicProp, onOpenThread, quickReactions, placeholder, showTopics, jumpTo: jumpToProp, search, scheduling, availability, emojibaseUrl, linkPreviewImages, linkPreviewProxyUrl, slots, className, }: ChatThreadProps): react.JSX.Element;
175
240
 
176
241
  interface ChatPanelNoSelectionProps {
177
242
  /** Words under the title; "Pick one on the left to read and reply." by default. */
@@ -193,9 +258,11 @@ interface ChatPanelProps {
193
258
  slots?: {
194
259
  noSelection?: SlotOverride<ChatPanelNoSelectionProps>;
195
260
  };
261
+ /** Search across every conversation, above the inbox. Default true. */
262
+ search?: boolean;
196
263
  className?: string;
197
264
  }
198
- declare function ChatPanel({ resolveUser, selectedId: controlled, defaultSelectedId, onSelectedChange, inboxHeader, inboxProps, threadProps, slots, className, }: ChatPanelProps): react.JSX.Element;
265
+ declare function ChatPanel({ resolveUser, selectedId: controlled, defaultSelectedId, onSelectedChange, inboxHeader, inboxProps, threadProps, slots, search, className, }: ChatPanelProps): react.JSX.Element;
199
266
 
200
267
  interface ChatComposerHandle {
201
268
  /** Adds files as if picked (a drop on the thread, say). Refused ones get a toast. */
@@ -206,7 +273,7 @@ interface ChatComposerHandle {
206
273
  }
207
274
  interface ChatComposerProps {
208
275
  /** From `useConversation`: how to send, and whether you may. */
209
- conversation: Pick<ConversationState, "send" | "signalTyping" | "signalRecording" | "role" | "canPost" | "accessLoading" | "conversation">;
276
+ conversation: Pick<ConversationState, "send" | "signalTyping" | "signalRecording" | "role" | "canPost" | "accessLoading" | "conversation"> & Partial<Pick<ConversationState, "accessError" | "refetchAccess" | "signalRecordingVideo" | "conversationLoading">>;
210
277
  /** The message being replied to; a banner shows above the box. */
211
278
  replyTo?: ChatMessage | null;
212
279
  onCancelReply?: () => void;
@@ -218,10 +285,22 @@ interface ChatComposerProps {
218
285
  /** Called after a message is queued. */
219
286
  onSent?: () => void;
220
287
  autoFocus?: boolean;
288
+ /** Shown instead of the box, with a lock (a closed topic). */
289
+ disabledReason?: string | null;
290
+ /** The topic new messages go to (scheduled ones too). */
291
+ topic?: string | null;
292
+ /** Offer scheduled send (the arrow beside Send) and list what's scheduled. Default true. */
293
+ scheduling?: boolean;
294
+ /** Offer video recording. Default true where the browser can record. */
295
+ video?: boolean;
296
+ /** Offer the emoji picker (only when `emojibaseUrl` is given). Default true. */
297
+ emoji?: boolean;
298
+ /** Where the emoji picker loads its data from. */
299
+ emojibaseUrl?: string;
221
300
  className?: string;
222
301
  ref?: Ref<ChatComposerHandle>;
223
302
  }
224
- declare function ChatComposer({ conversation: state, replyTo, onCancelReply, resolveUser, placeholder, accessory, onSent, autoFocus, className, ref, }: ChatComposerProps): react.JSX.Element;
303
+ declare function ChatComposer({ conversation: state, replyTo, onCancelReply, resolveUser, placeholder, accessory, onSent, autoFocus, disabledReason, topic, scheduling, video, emoji, emojibaseUrl, className, ref, }: ChatComposerProps): react.JSX.Element;
225
304
 
226
305
  interface MessageRowProps {
227
306
  item: ThreadItem;
@@ -357,6 +436,12 @@ interface MemoryConversationSeed {
357
436
  attachments?: Partial<ChatAttachment>[];
358
437
  })[];
359
438
  pins?: string[];
439
+ /** Its topics, in order. */
440
+ topics?: {
441
+ topic: string;
442
+ title?: string;
443
+ closed?: boolean;
444
+ }[];
360
445
  }
361
446
  interface MemoryChatOptions {
362
447
  /** The signed-in user. */
@@ -366,6 +451,10 @@ interface MemoryChatOptions {
366
451
  files?: Record<string, string>;
367
452
  /** Milliseconds each call waits, to show loading states. Default 0. */
368
453
  latencyMs?: number;
454
+ /** Away messages and working hours, by user id. */
455
+ availability?: Record<string, Partial<Omit<ChatAvailability, "userId">>>;
456
+ /** Link previews the "server" has fetched. */
457
+ linkPreviews?: Partial<ChatLinkPreview$1>[];
369
458
  }
370
459
  type MemoryChatClient = ChatClient & {
371
460
  /** Someone else posts (a demo's other side). */
@@ -376,6 +465,10 @@ type MemoryChatClient = ChatClient & {
376
465
  failNextSends(count?: number): void;
377
466
  /** The next send waits until the returned function is called (a test watching the sending state). */
378
467
  holdNextSend(): () => void;
468
+ /** Posts every scheduled message due by `now` (a demo's clock), as the server's runner does. */
469
+ sendDue(now?: Date): Promise<number>;
470
+ /** Moderation's verdict on a message (the server's side). */
471
+ moderate(messageId: string, result: "clean" | "flagged" | "hidden"): void;
379
472
  /** Every call made, for tests. */
380
473
  calls: {
381
474
  method: string;
@@ -384,4 +477,214 @@ type MemoryChatClient = ChatClient & {
384
477
  };
385
478
  declare function createMemoryChatClient(options: MemoryChatOptions): MemoryChatClient;
386
479
 
387
- export { type AttachmentView, type ChatAttachmentSlotProps, ChatComposer, type ChatComposerAccessoryProps, type ChatComposerHandle, type ChatComposerProps, ChatInbox, type ChatInboxEmptyProps, type ChatInboxProps, ChatMarkdown, type ChatMarkdownProps, ChatPanel, type ChatPanelNoSelectionProps, type ChatPanelProps, ChatThread, type ChatThreadEmptyProps, type ChatThreadHeaderProps, type ChatThreadProps, type ChatThreadSlots, type ChatUser, FileChip, GenericAttachment, ImageGrid, type ImageGridProps, Lightbox, type LightboxProps, type MarkdownNode, type MemoryChatClient, type MemoryChatOptions, type MemoryConversationSeed, MessageAttachments, type MessageAttachmentsProps, MessageRow, type MessageRowProps, QUICK_REACTIONS, type ResolveUser, type UseVoiceRecorderOptions, VideoAttachment, VoiceNote, type VoiceNoteProps, type VoiceRecorder, type VoiceRecorderState, type VoiceRecording, clockTime, createMemoryChatClient, formatBytes, inboxTitle, isSafeHref, messageLabel, parseMarkdown, pickVoiceMimeType, plainText, relativeTime, toAttachmentView, useVoiceRecorder, voiceFile };
480
+ interface ChatTopicBarProps {
481
+ topics: ChatTopic[];
482
+ /** The topic shown; null for "All". */
483
+ value: string | null;
484
+ onValueChange: (topic: string | null) => void;
485
+ /** Owners and admins: "New topic", and close or reopen. */
486
+ canManage?: boolean;
487
+ /** Saves a topic (from useTopics). */
488
+ onSetTopic?: (topic: string, input: TopicInput) => Promise<void>;
489
+ className?: string;
490
+ }
491
+ declare function ChatTopicBar({ topics, value, onValueChange, canManage, onSetTopic, className }: ChatTopicBarProps): react.JSX.Element;
492
+
493
+ interface ChatScheduleMenuProps {
494
+ /** Called with the chosen time. */
495
+ onSchedule: (at: Date) => void;
496
+ disabled?: boolean;
497
+ /** For tests and demos. */
498
+ now?: Date;
499
+ }
500
+ /** `YYYY-MM-DDTHH:mm` in local time, for a datetime-local input. */
501
+ declare function toLocalInput(d: Date): string;
502
+ /** The chevron beside Send: "Schedule message" with quick picks and a custom time. */
503
+ declare function ChatScheduleMenu({ onSchedule, disabled, now }: ChatScheduleMenuProps): react.JSX.Element;
504
+ interface ChatScheduledListProps {
505
+ /** One conversation's; all of yours without it. */
506
+ conversationId?: string | null;
507
+ /** Names a conversation in the all-conversations list. */
508
+ conversationTitle?: (conversationId: string) => string;
509
+ className?: string;
510
+ }
511
+ /** Your scheduled messages, soonest first, each with when it goes and Cancel. */
512
+ declare function ChatScheduledList({ conversationId, conversationTitle, className }: ChatScheduledListProps): react.JSX.Element;
513
+
514
+ interface ChatAwayBannerProps {
515
+ /** One person (a direct conversation). */
516
+ availability?: ChatAvailability | null;
517
+ /** The person's name, as the app shows it. */
518
+ name?: string;
519
+ /** Several people (a group): everyone away or outside their hours is named, two at most. */
520
+ people?: {
521
+ availability: ChatAvailability | null | undefined;
522
+ name: string;
523
+ }[];
524
+ now?: Date;
525
+ className?: string;
526
+ }
527
+ /** "Mark replies Mon–Fri, 9–5 (Eastern)", "Away until Monday: Back Monday", "Sam, Lee and 2 others are away"; nothing when everyone is around. */
528
+ declare function ChatAwayBanner({ availability, name, people, now, className }: ChatAwayBannerProps): react.JSX.Element | null;
529
+ interface ChatAvailabilitySettingsProps {
530
+ /** Called after a save. */
531
+ onSaved?: () => void;
532
+ className?: string;
533
+ }
534
+ /**
535
+ * Your availability: an away message (and until when), working hours per weekday, and the
536
+ * time zone they're read in. One range per day here; extra ranges saved elsewhere are kept
537
+ * for the days you don't change.
538
+ */
539
+ declare function ChatAvailabilitySettings({ onSaved, className }: ChatAvailabilitySettingsProps): react.JSX.Element;
540
+
541
+ interface ChatBroadcastProps {
542
+ resolveUser: ResolveUser;
543
+ /** Only this kind of conversation (`direct`, say). */
544
+ kind?: string;
545
+ /** Which inbox rows may be picked; every one by default. */
546
+ filter?: (item: ChatInboxItem) => boolean;
547
+ /** Called with how many it went to. */
548
+ onSent?: (sent: number, picked: number) => void;
549
+ className?: string;
550
+ }
551
+ declare function ChatBroadcast({ resolveUser, kind, filter, onSent, className }: ChatBroadcastProps): react.JSX.Element;
552
+
553
+ interface ChatSearchProps {
554
+ resolveUser: ResolveUser;
555
+ /** Search one conversation; all of yours without it. */
556
+ conversationId?: string | null;
557
+ /** Called with the chosen result. */
558
+ onOpen: (hit: ChatSearchHit) => void;
559
+ /** Names a conversation, on results across conversations. */
560
+ conversationTitle?: (conversationId: string) => string;
561
+ /** Start with the box focused. */
562
+ autoFocus?: boolean;
563
+ debounceMs?: number;
564
+ className?: string;
565
+ }
566
+ /** A snippet with its « » marks as <mark>. */
567
+ declare function SearchSnippet({ snippet, className }: {
568
+ snippet: string;
569
+ className?: string;
570
+ }): react.JSX.Element;
571
+ declare function ChatSearch({ resolveUser, conversationId, onOpen, conversationTitle, autoFocus, debounceMs, className }: ChatSearchProps): react.JSX.Element;
572
+
573
+ /**
574
+ * @feat:chat
575
+ * `ChatErrorBoundary`: what one message (or one app attachment card) draws can't take the whole
576
+ * thread down. Messages carry what other people sent, and app renderers get sender-controlled
577
+ * data, so a render that throws shows its fallback instead, and the error goes to `onError`
578
+ * (console.error by default) so it is never silent.
579
+ */
580
+
581
+ interface ChatErrorBoundaryProps {
582
+ /** Drawn instead of the children once they throw. */
583
+ fallback: ReactNode;
584
+ /** Told about the error; console.error by default. */
585
+ onError?: (error: unknown, info: ErrorInfo) => void;
586
+ children: ReactNode;
587
+ }
588
+ declare class ChatErrorBoundary extends Component<ChatErrorBoundaryProps, {
589
+ failed: boolean;
590
+ }> {
591
+ state: {
592
+ failed: boolean;
593
+ };
594
+ static getDerivedStateFromError(): {
595
+ failed: boolean;
596
+ };
597
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
598
+ render(): ReactNode;
599
+ }
600
+
601
+ /** The picker can load: an emojibase URL was given. */
602
+ declare const canPickEmoji: (emojibaseUrl: string | null | undefined) => emojibaseUrl is string;
603
+ interface ChatEmojiPickerProps {
604
+ onSelect: (emoji: string) => void;
605
+ /** Where the Emojibase data is fetched from (`${emojibaseUrl}/en/data.json`). Required to load anything. */
606
+ emojibaseUrl?: string;
607
+ locale?: "en";
608
+ columns?: number;
609
+ className?: string;
610
+ }
611
+ declare function ChatEmojiPicker({ onSelect, emojibaseUrl, locale, columns, className }: ChatEmojiPickerProps): react.JSX.Element;
612
+
613
+ type VideoRecorderState = "idle" | "requesting" | "previewing" | "recording" | "recorded" | "denied" | "unsupported" | "error";
614
+ interface VideoRecording {
615
+ blob: Blob;
616
+ /** Without codec parameters: `video/webm` or `video/mp4`. */
617
+ contentType: string;
618
+ name: string;
619
+ durationMs: number;
620
+ /** An object URL for playback before sending. */
621
+ url: string | null;
622
+ }
623
+ /** MP4 where the browser records it (Safari, recent Chrome), else WebM. */
624
+ declare function pickVideoMimeType(): string | null;
625
+ interface UseVideoRecorderOptions {
626
+ /** It stops itself here (`recordingLimitMs(role)`). */
627
+ maxMs: number;
628
+ /** About every second while recording (to tell the others). */
629
+ onTick?: () => void;
630
+ }
631
+ declare function useVideoRecorder({ maxMs, onTick }: UseVideoRecorderOptions): {
632
+ state: VideoRecorderState;
633
+ error: string | null;
634
+ elapsedMs: number;
635
+ maxMs: number;
636
+ stream: MediaStream | null;
637
+ recording: VideoRecording | null;
638
+ open: () => Promise<boolean>;
639
+ start: () => Promise<void>;
640
+ stop: () => void;
641
+ retake: () => Promise<void>;
642
+ reset: () => void;
643
+ handOff: () => void;
644
+ };
645
+ type VideoRecorder = ReturnType<typeof useVideoRecorder>;
646
+ /** A recording as a file the outbox uploads to storage (the fallback without `videoUpload`). */
647
+ declare function videoFile(rec: VideoRecording): LocalFile;
648
+ /**
649
+ * Uploads a recording through the app's `videoUpload` (a Mux direct upload: PUT the bytes to
650
+ * the URL it returns) and gives back the attachment to send, pending until Mux's webhook marks
651
+ * it ready.
652
+ */
653
+ declare function uploadVideo(videoUpload: ChatVideoUpload, conversationId: string, rec: Pick<VideoRecording, "blob" | "contentType" | "durationMs" | "name">, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<AttachmentInput>;
654
+ interface ChatVideoRecorderProps {
655
+ open: boolean;
656
+ onOpenChange: (open: boolean) => void;
657
+ conversationId: string;
658
+ maxMs: number;
659
+ /** From useConversation: sends through the outbox. */
660
+ send: (input: Omit<SendInput, "conversationId" | "clientId">, files?: LocalFile[]) => Promise<unknown>;
661
+ /** Merged into what's sent (a reply, a topic, an internal note). */
662
+ extra?: Omit<SendInput, "conversationId" | "clientId" | "attachments" | "body">;
663
+ videoUpload?: ChatVideoUpload;
664
+ onTick?: () => void;
665
+ onSent?: () => void;
666
+ /** For tests. */
667
+ fetchImpl?: typeof fetch;
668
+ }
669
+ /** The composer's video dialog: preview, record (to the role's limit), review, retake, send. */
670
+ declare function ChatVideoRecorder({ open, onOpenChange, conversationId, maxMs, send, extra, videoUpload, onTick, onSent, fetchImpl }: ChatVideoRecorderProps): react.JSX.Element;
671
+ declare const muxStreamUrl: (playbackId: string) => string;
672
+ declare const muxPosterUrl: (playbackId: string) => string;
673
+ interface ChatVideoPlayerProps {
674
+ /** A file URL (storage), when there is no playback id. */
675
+ src?: string | null;
676
+ /** Mux's playback id: streamed as HLS. */
677
+ playbackId?: string | null;
678
+ poster?: string | null;
679
+ /** Mux is still working on it. */
680
+ processing?: boolean;
681
+ failed?: boolean;
682
+ /** Still uploading from this device. */
683
+ uploading?: boolean;
684
+ label?: string;
685
+ className?: string;
686
+ }
687
+ /** Plays a chat video: Mux HLS (native, else hls.js loaded on demand), or a file. */
688
+ declare function ChatVideoPlayer({ src, playbackId, poster, processing, failed, uploading, label, className }: ChatVideoPlayerProps): react.JSX.Element;
689
+
690
+ export { type AttachmentView, ChatAttachmentCard, type ChatAttachmentCardProps, type ChatAttachmentSlotProps, ChatAvailabilitySettings, type ChatAvailabilitySettingsProps, ChatAwayBanner, type ChatAwayBannerProps, ChatBroadcast, type ChatBroadcastProps, ChatComposer, type ChatComposerAccessoryProps, type ChatComposerHandle, type ChatComposerProps, ChatEmojiPicker, type ChatEmojiPickerProps, ChatErrorBoundary, type ChatErrorBoundaryProps, ChatInbox, type ChatInboxEmptyProps, type ChatInboxProps, ChatLinkPreview, type ChatLinkPreviewProps, ChatMarkdown, type ChatMarkdownProps, ChatPanel, type ChatPanelNoSelectionProps, type ChatPanelProps, ChatScheduleMenu, type ChatScheduleMenuProps, ChatScheduledList, type ChatScheduledListProps, ChatSearch, type ChatSearchProps, ChatThread, type ChatThreadEmptyProps, type ChatThreadHeaderProps, type ChatThreadProps, type ChatThreadSlots, ChatTopicBar, type ChatTopicBarProps, type ChatUser, ChatVideoPlayer, type ChatVideoPlayerProps, ChatVideoRecorder, type ChatVideoRecorderProps, FileChip, GenericAttachment, ImageGrid, type ImageGridProps, Lightbox, type LightboxProps, type LinkPreviewImages, type MarkdownNode, type MemoryChatClient, type MemoryChatOptions, type MemoryConversationSeed, MessageAttachments, type MessageAttachmentsProps, MessageRow, type MessageRowProps, QUICK_REACTIONS, type ResolveUser, SearchSnippet, type UseVideoRecorderOptions, type UseVoiceRecorderOptions, VideoAttachment, type VideoRecorder, type VideoRecorderState, type VideoRecording, VoiceNote, type VoiceNoteProps, type VoiceRecorder, type VoiceRecorderState, type VoiceRecording, canPickEmoji, clockTime, createMemoryChatClient, formatBytes, inboxTitle, isSafeHref, linkPreviewImageSrc, messageLabel, muxPosterUrl, muxStreamUrl, parseMarkdown, pickVideoMimeType, pickVoiceMimeType, plainText, relativeTime, toAttachmentView, toLocalInput, uploadVideo, useVideoRecorder, useVoiceRecorder, videoFile, voiceFile };