@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
@@ -0,0 +1,151 @@
1
+ import { b as AttachmentKind, A as AttachmentInput, i as ChatClient, B as PendingMessage, P as PendingFile, q as ChatMessage, E as SendInput } from './client-DMa2j7kz.js';
2
+
3
+ /**
4
+ * @feat:chat
5
+ * Media rules shared by every platform: which files may be sent (the same list
6
+ * migration 0001 checks), what kind an upload becomes, the default limits, and
7
+ * the waveform a voice note carries so its bubble draws before the audio loads.
8
+ */
9
+
10
+ /**
11
+ * A random UUID (v4): `crypto.randomUUID` where the platform has it (some React Native
12
+ * runtimes don't), else `crypto.getRandomValues`, else Math.random as a last resort. Ids
13
+ * only need to be unique per sender: the server checks (sender, client_id).
14
+ */
15
+ declare function randomId(): string;
16
+ /** Mirrors genesis_chat.allowed_extension. Never SVG or HTML, which a browser runs as a page. */
17
+ 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"];
18
+ /** The extension a file is stored under: from its content type, else its name. */
19
+ declare function fileExtension(name: string, contentType?: string): string;
20
+ declare function isAllowedExtension(ext: string): boolean;
21
+ /** What an upload becomes in a message. */
22
+ declare function attachmentKindFor(contentType: string, opts?: {
23
+ recordedVoice?: boolean;
24
+ }): AttachmentKind;
25
+ /** Default limits (decision 5): an app may lower them, not raise them past the bucket's 100 MB. */
26
+ declare const CHAT_LIMITS: {
27
+ /** Voice notes and videos, for members. */
28
+ readonly recordingMs: number;
29
+ /** For owners, admins and observers (coaches, staff). */
30
+ readonly staffRecordingMs: number;
31
+ readonly fileBytes: number;
32
+ readonly videoBytes: number;
33
+ readonly attachmentsPerMessage: 20;
34
+ readonly bodyChars: 10000;
35
+ };
36
+ /** The longest recording this role may make. */
37
+ declare function recordingLimitMs(role: string | null | undefined): number;
38
+ /**
39
+ * Peaks for a waveform: `count` values 0–100 from raw samples (-1…1), the loudest
40
+ * in each bucket, scaled so the loudest bucket is 100. Quiet recordings still show
41
+ * a shape; silence shows a flat line.
42
+ */
43
+ declare function waveformPeaks(samples: ArrayLike<number>, count?: number): number[];
44
+ /**
45
+ * Peaks from a level meter read while recording (expo-audio's `metering`, in dB,
46
+ * about -160…0): resampled to `count` values 0–100.
47
+ */
48
+ declare function waveformFromMetering(levelsDb: number[], count?: number): number[];
49
+ /** 0:42, 1:05:09. */
50
+ declare function formatDuration(ms: number | null | undefined): string;
51
+ /** Playback speeds offered for voice and video (free for everyone, decision 5). */
52
+ declare const PLAYBACK_RATES: readonly [1, 1.5, 2];
53
+ declare function nextPlaybackRate(rate: number): number;
54
+
55
+ /**
56
+ * @feat:chat
57
+ * The send queue: a message shows at once as pending, its files upload, then it
58
+ * is sent with the clientId made when it was written, so a retry (after a lost
59
+ * connection, a closed app) never posts twice. Messages to one conversation go
60
+ * in the order they were written: the first that can't go (a lost file, a refusal)
61
+ * holds back the ones after it. A refusal (not allowed, bad data) is `permanent`
62
+ * and only sent again by hand. `clear()` (sign-out) stops sends in flight: none
63
+ * posts, or saves anything, afterwards. Storage is the platform's (memory by default;
64
+ * localStorage on web, MMKV or SQLite on native), behind OutboxStore.
65
+ */
66
+
67
+ /** A file still on the device, uploaded when its message is sent. */
68
+ interface LocalFile {
69
+ /** The bytes, or (native) a file URI the platform reads in `readFile`. */
70
+ data: Blob | ArrayBuffer | string;
71
+ name: string;
72
+ contentType: string;
73
+ /** A preview to show while it uploads (an object URL, a file URI). The outbox owns it from here: see `releasePreview`. */
74
+ previewUrl?: string;
75
+ /** Known up front: a voice note's duration and waveform, a photo's size. */
76
+ meta?: Partial<Omit<AttachmentInput, "path" | "kind">> & {
77
+ kind?: AttachmentInput["kind"];
78
+ };
79
+ }
80
+ interface QueuedMessage extends Omit<PendingMessage, "files"> {
81
+ files: LocalFile[];
82
+ /** What each file is, kept with the saved copy (PendingMessage.files). */
83
+ pendingFiles?: PendingFile[];
84
+ /** Files lost on a relaunch (bytes that weren't persisted). */
85
+ lostFiles?: number;
86
+ attempts: number;
87
+ }
88
+ interface OutboxStore {
89
+ load(): Promise<QueuedMessage[]>;
90
+ save(queue: QueuedMessage[]): Promise<void>;
91
+ }
92
+ declare const memoryOutboxStore: () => OutboxStore;
93
+ interface OutboxOptions {
94
+ client: ChatClient;
95
+ userId: string;
96
+ store?: OutboxStore;
97
+ /** Reads a native file URI into bytes for upload. */
98
+ readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
99
+ /** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts when the message is discarded or the outbox cleared. */
100
+ upload?: (conversationId: string, file: LocalFile, opts?: {
101
+ signal?: AbortSignal;
102
+ }) => Promise<string>;
103
+ /** Called with every change, for the UI. */
104
+ onChange?: (queue: QueuedMessage[]) => void;
105
+ /** Called when a message lands. */
106
+ onSent?: (message: ChatMessage, clientId: string) => void;
107
+ /** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */
108
+ newId?: () => string;
109
+ /**
110
+ * Keeps a file held as bytes (a pasted image, a recording) between launches: write it
111
+ * somewhere and return a URI `readFile` can read (native: the app's cache directory).
112
+ * Without it, such a file isn't saved, and after a relaunch its message asks for it again.
113
+ */
114
+ persistFile?: (file: LocalFile) => Promise<string>;
115
+ /**
116
+ * Lets go of a file's `previewUrl` once its message has landed, been discarded or cleared.
117
+ * Default: revokes `blob:` object URLs (the web's), and leaves everything else alone.
118
+ */
119
+ releasePreview?: (url: string) => void;
120
+ }
121
+ /** Revokes a `blob:` object URL; anything else (a native file URI) is left alone. */
122
+ declare function releaseObjectUrl(url: string): void;
123
+ declare function createOutbox(opts: OutboxOptions): {
124
+ /** Queues a message; it shows at once and sends in order. Returns its clientId. */
125
+ send(input: Omit<SendInput, "clientId"> & {
126
+ clientId?: string;
127
+ }, files?: LocalFile[]): Promise<string>;
128
+ retry(clientId: string): Promise<void>;
129
+ /**
130
+ * Gives a message the files it lost on a relaunch and sends it. `files` fill the lost
131
+ * slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass
132
+ * one per lost slot; with fewer, the message keeps waiting for the rest.
133
+ */
134
+ reattach(clientId: string, files: LocalFile[]): Promise<void>;
135
+ /**
136
+ * Retries failed messages (after the connection comes back), in order per conversation,
137
+ * stopping at the first that can't go on its own: one waiting for a lost file, or one the
138
+ * server refused (`permanent`, retried only by hand). The ones after it keep waiting.
139
+ */
140
+ retryAll(): Promise<void>;
141
+ /** Forgets every queued message, here and in the store (call it on sign-out). */
142
+ clear(): Promise<void>;
143
+ discard(clientId: string): Promise<void>;
144
+ pending(conversationId?: string): PendingMessage[];
145
+ /** A new client id, made the way this outbox makes them. */
146
+ newId: typeof randomId;
147
+ load: () => Promise<void>;
148
+ };
149
+ type Outbox = ReturnType<typeof createOutbox>;
150
+
151
+ export { ALLOWED_EXTENSIONS as A, CHAT_LIMITS as C, type LocalFile as L, type Outbox as O, PLAYBACK_RATES as P, type QueuedMessage as Q, type OutboxOptions as a, type OutboxStore as b, attachmentKindFor as c, createOutbox as d, formatDuration as e, fileExtension as f, recordingLimitMs as g, releaseObjectUrl as h, isAllowedExtension as i, waveformPeaks as j, memoryOutboxStore as m, nextPlaybackRate as n, randomId as r, waveformFromMetering as w };
package/dist/push.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { i as ChatClient } from './client-DMa2j7kz.js';
2
+ import { O as Outbox, b as OutboxStore, Q as QueuedMessage } from './outbox-B0y3phE0.js';
3
+ import '@supabase/supabase-js';
4
+
5
+ /**
6
+ * @feat:chat
7
+ * Push glue for genesis-native's push block, data only: the `messages`
8
+ * category (a switch in Notification settings and an Android channel), its
9
+ * notification actions (Reply with a text field, Mark read), what a tap opens
10
+ * (`data.url` = `/messages/<conversationId>`), the reply and mark-read
11
+ * handlers for the block's `onResponse`, and the match that keeps a thread's
12
+ * own pushes out of the foreground while it is open (the block's `suppress`).
13
+ *
14
+ * const push = createPush({ …, categories: [...yours, chatPushCategory()],
15
+ * onResponse: (r) => void handleChatPushResponse(r, { client: chatClient, notifications: Notifications }) });
16
+ * void registerChatNotificationActions(Notifications); // once, at start
17
+ * useSuppressNotifications(chatThreadMatch(conversationId)); // in the thread screen
18
+ *
19
+ * The sender sets `categoryId: "messages"` (iOS) / the `messages` channel
20
+ * (Android) and `data: { url: "/messages/<id>", conversationId, seq, messageId }`;
21
+ * `chatPushPayload` builds those and the fields iOS rich push reads (the
22
+ * sender's name and photo, the group, a photo), and it is server safe: import
23
+ * it from `@marktiderman/genesis-chat/push`.
24
+ *
25
+ * A reply goes through the outbox, so a phone that is offline keeps it: pass
26
+ * the live one (`useChat().outbox`) when the chat screens are mounted, and the
27
+ * persisted store and user id (`ChatProvider outboxStore`) for when they
28
+ * aren't. A reply that can't be sent then waits there, failed, with Retry in
29
+ * its thread.
30
+ */
31
+
32
+ declare const CHAT_PUSH_CATEGORY = "messages";
33
+ declare const CHAT_REPLY_ACTION = "reply";
34
+ declare const CHAT_MARK_READ_ACTION = "mark-read";
35
+ interface ChatPushCopy {
36
+ title: string;
37
+ description: string;
38
+ reply: string;
39
+ replySubmit: string;
40
+ replyPlaceholder: string;
41
+ markRead: string;
42
+ }
43
+ declare const chatPushCopy: ChatPushCopy;
44
+ /** The path a message's push opens. */
45
+ declare function chatPath(conversationId: string): string;
46
+ /** The category for createPush's `categories` (genesis-native's NotificationCategory). */
47
+ declare function chatPushCategory(copy?: Partial<ChatPushCopy>): {
48
+ id: string;
49
+ title: string;
50
+ description: string;
51
+ importance: "high";
52
+ };
53
+ /** expo-notifications' `setNotificationCategoryAsync`, the one call this needs. */
54
+ interface NotificationCategoriesLike {
55
+ setNotificationCategoryAsync(identifier: string, actions: {
56
+ identifier: string;
57
+ buttonTitle: string;
58
+ textInput?: {
59
+ submitButtonTitle: string;
60
+ placeholder: string;
61
+ };
62
+ options?: {
63
+ opensAppToForeground?: boolean;
64
+ isDestructive?: boolean;
65
+ isAuthenticationRequired?: boolean;
66
+ };
67
+ }[], options?: Record<string, unknown>): Promise<unknown>;
68
+ dismissNotificationAsync?(identifier: string): Promise<void>;
69
+ }
70
+ /** The actions on a message's notification: Reply (a text field) and Mark read, neither opening the app. */
71
+ declare function chatNotificationActions(copy?: Partial<ChatPushCopy>): ({
72
+ identifier: string;
73
+ buttonTitle: string;
74
+ textInput: {
75
+ submitButtonTitle: string;
76
+ placeholder: string;
77
+ };
78
+ options: {
79
+ opensAppToForeground: boolean;
80
+ };
81
+ } | {
82
+ identifier: string;
83
+ buttonTitle: string;
84
+ options: {
85
+ opensAppToForeground: boolean;
86
+ };
87
+ textInput?: undefined;
88
+ })[];
89
+ /** Registers the actions with the OS (once, at start; it is idempotent). */
90
+ declare function registerChatNotificationActions(notifications: NotificationCategoriesLike, copy?: Partial<ChatPushCopy>, onError?: (error: unknown) => void): Promise<unknown>;
91
+ /** The conversation a push is about: `data.conversationId`, else the id in `data.url`. */
92
+ declare function conversationIdFromData(data: Record<string, unknown> | null | undefined): string | null;
93
+ /**
94
+ * The client id a reply from this notification sends with: the notification's
95
+ * own id, as a uuid (it already is one on iOS; anything else is hashed into
96
+ * one), so the same reply delivered twice posts once.
97
+ */
98
+ declare function replyClientId(notificationId: string): string;
99
+ /**
100
+ * The client id for one reply: from the notification's id, the message it is
101
+ * about (`data.messageId`) and the reply's text. The same reply delivered
102
+ * twice posts once; two replies to one conversation never share an id, even
103
+ * when iOS reuses the notification's id for a collapsed push.
104
+ */
105
+ declare function replyClientIdFor(notificationId: string, data: Record<string, unknown> | null | undefined, body: string): string;
106
+ /** The part of a notification response this reads (expo-notifications' `NotificationResponse`). */
107
+ interface ChatPushResponseLike {
108
+ actionIdentifier: string;
109
+ /** The text typed into Reply. */
110
+ userText?: string;
111
+ notification: {
112
+ request: {
113
+ identifier: string;
114
+ content: {
115
+ data?: Record<string, unknown> | null;
116
+ };
117
+ };
118
+ };
119
+ }
120
+ /** "queued": the reply couldn't go now and waits in the outbox, failed, for Retry. */
121
+ type ChatPushOutcome = "replied" | "queued" | "marked-read" | "ignored" | "failed";
122
+ type LiveOutbox = Pick<Outbox, "send" | "pending"> & Partial<Pick<Outbox, "load">>;
123
+ /** How long a reply queued in the live outbox is waited for before it counts as queued, not sent. */
124
+ declare const PUSH_REPLY_WAIT_MS = 15000;
125
+ /** Where a reply waits when it can't go now. */
126
+ interface ChatPushOutboxDeps {
127
+ /** The live outbox (`useChat().outbox`), or a getter for it (read again after a slow failure): the reply is queued there and sent in order. */
128
+ outbox?: LiveOutbox | null | (() => LiveOutbox | null | undefined);
129
+ /** Without a live outbox: the persisted store (`ChatProvider outboxStore`) a reply that failed is added to. */
130
+ store?: OutboxStore | null;
131
+ /** Whose store it is (the signed-in user's id). */
132
+ userId?: string | null;
133
+ }
134
+ /** A reply that couldn't be sent, as the outbox keeps it: shown failed in its thread, with Retry. */
135
+ declare function failedReply(input: {
136
+ conversationId: string;
137
+ clientId: string;
138
+ body: string;
139
+ }, userId: string, error?: string): QueuedMessage;
140
+ /**
141
+ * Handles Reply and Mark read from a message's notification (pass it as the
142
+ * push block's `onResponse`). Taps on the notification itself are the block's
143
+ * routing, not this. Never throws: a failure is the outcome "failed" (and
144
+ * `onError`).
145
+ */
146
+ declare function handleChatPushResponse(response: ChatPushResponseLike, deps: {
147
+ client: Pick<ChatClient, "send" | "markRead">;
148
+ notifications?: Pick<NotificationCategoriesLike, "dismissNotificationAsync">;
149
+ onError?: (error: unknown) => void;
150
+ /** How long to wait for a reply queued in the live outbox to go. Default PUSH_REPLY_WAIT_MS. */
151
+ waitMs?: number;
152
+ } & ChatPushOutboxDeps): Promise<ChatPushOutcome>;
153
+ /** For the push block's `suppress` / `useSuppressNotifications`: this conversation's pushes, while its thread is open. */
154
+ declare function chatThreadMatch(conversationId: string | null | undefined): ((data: Record<string, unknown>) => boolean) | null;
155
+ /** What a message's push says about itself, for `chatPushPayload`. */
156
+ interface ChatPushPayloadInput {
157
+ conversationId: string;
158
+ messageId: string;
159
+ seq?: number | null | undefined;
160
+ /** Who sent it: an opaque id (never an email or phone), the name to show and an https photo. */
161
+ sender?: {
162
+ id: string;
163
+ name: string;
164
+ avatarUrl?: string | null | undefined;
165
+ } | null | undefined;
166
+ /** The conversation's name when it is a group; leave it out for a direct conversation. */
167
+ groupName?: string | null | undefined;
168
+ /** An https photo in the message, attached to the notification. */
169
+ imageUrl?: string | null | undefined;
170
+ /**
171
+ * The recipient hides previews: the push carries the link and ids only (no
172
+ * sender, group or photo) and the service extension doesn't run. Send neutral
173
+ * title and body text with it.
174
+ */
175
+ hidePreviews?: boolean | undefined;
176
+ }
177
+ /** The `data` of a message's push. Flat strings and numbers, as genesis-notifications' `PushMessage.data` takes. */
178
+ interface ChatPushData {
179
+ [key: string]: string | number;
180
+ url: string;
181
+ conversationId: string;
182
+ messageId: string;
183
+ }
184
+ /** The fields to spread into an Expo push message (with `to`, `title`, `body`, `badge`, `channelId`). */
185
+ interface ChatPushPayload {
186
+ categoryId: typeof CHAT_PUSH_CATEGORY;
187
+ /** Groups the conversation's notifications, and is the conversation iOS files a Communication Notification under. */
188
+ threadId: string;
189
+ /** iOS runs genesis-native's Notification Service Extension only when this is true (`mutable-content: 1`). */
190
+ mutableContent: boolean;
191
+ data: ChatPushData;
192
+ }
193
+ /**
194
+ * The category, thread, `mutableContent` and `data` of a message's push, for
195
+ * the server that sends it:
196
+ *
197
+ * const message = { to, title, body, badge, channelId: "messages", ...chatPushPayload({ conversationId, messageId, seq, sender, groupName, imageUrl, hidePreviews }) };
198
+ *
199
+ * On iOS, with the rich push plugin, a push with a sender is a Communication
200
+ * Notification (the sender's photo; Focus treats it as a message) and `imageUrl`
201
+ * is attached. Fields that aren't usable (a URL that isn't https, an empty
202
+ * name) are left out rather than sent.
203
+ */
204
+ declare function chatPushPayload(input: ChatPushPayloadInput): ChatPushPayload;
205
+
206
+ export { CHAT_MARK_READ_ACTION, CHAT_PUSH_CATEGORY, CHAT_REPLY_ACTION, type ChatPushCopy, type ChatPushData, type ChatPushOutboxDeps, type ChatPushOutcome, type ChatPushPayload, type ChatPushPayloadInput, type ChatPushResponseLike, type NotificationCategoriesLike, PUSH_REPLY_WAIT_MS, chatNotificationActions, chatPath, chatPushCategory, chatPushCopy, chatPushPayload, chatThreadMatch, conversationIdFromData, failedReply, handleChatPushResponse, registerChatNotificationActions, replyClientId, replyClientIdFor };
package/dist/push.js ADDED
@@ -0,0 +1,3 @@
1
+ export { CHAT_MARK_READ_ACTION, CHAT_PUSH_CATEGORY, CHAT_REPLY_ACTION, PUSH_REPLY_WAIT_MS, chatNotificationActions, chatPath, chatPushCategory, chatPushCopy, chatPushPayload, chatThreadMatch, conversationIdFromData, failedReply, handleChatPushResponse, registerChatNotificationActions, replyClientId, replyClientIdFor } from './chunk-VEPZROQB.js';
2
+ //# sourceMappingURL=push.js.map
3
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"push.js"}
package/dist/react.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as _tanstack_query_core from '@tanstack/query-core';
2
- import { C as ChatClient, a as ChatConversation, b as ChatMember, c as ChatRole, d as ChatKindRules, e as ChatMessage, f as ChatActivity, S as SendInput, g as ChatSignal, h as ChatInboxItem } from './client-BtdALFCx.js';
2
+ import { h as ChatAttachment, i as ChatClient, k as ChatConversation, p as ChatMember, t as ChatRole, n as ChatKindRules, q as ChatMessage, g as ChatActivity, E as SendInput, M as MessagePage, C as ChatAvailability, d as AvailabilityInput, v as ChatSignal, m as ChatInboxItem, o as ChatLinkPreview, D as ScheduledMessage, S as ScheduleInput, u as ChatSearchHit, x as ChatTopic, T as TopicInput } from './client-DMa2j7kz.js';
3
3
  import * as react from 'react';
4
4
  import { ReactNode } from 'react';
5
- import { OutboxStore, LocalFile, ThreadItem, Outbox, QueuedMessage } from './index.js';
5
+ import { b as OutboxStore, L as LocalFile, O as Outbox, Q as QueuedMessage } from './outbox-B0y3phE0.js';
6
+ import { T as ThreadItem } from './thread-DWXHKPdw.js';
6
7
  import '@supabase/supabase-js';
7
8
 
8
9
  declare const chatKeys: {
@@ -14,15 +15,58 @@ declare const chatKeys: {
14
15
  role: (id: string) => readonly ["genesis-chat", "role", string];
15
16
  pins: (id: string) => readonly ["genesis-chat", "pins", string];
16
17
  files: (paths: string[]) => readonly ["genesis-chat", "files", string[]];
18
+ /** Under the conversation's key, so its `conversation` hint (set_topic broadcasts one) refreshes them. */
19
+ topics: (id: string) => readonly ["genesis-chat", "conversation", string, "topics"];
20
+ scheduled: (conversationId?: string | null) => readonly ["genesis-chat", "scheduled", string | null];
21
+ availability: (conversationId: string) => readonly ["genesis-chat", "conversation", string, "availability"];
22
+ myAvailability: () => readonly ["genesis-chat", "my-availability"];
23
+ search: (query: string, conversationId?: string | null) => readonly ["genesis-chat", "search", string | null, string];
24
+ previews: (messageIds: string[]) => readonly ["genesis-chat", "previews", string[]];
25
+ /** One message's link previews (useLinkPreviews keeps one query per message). */
26
+ preview: (messageId: string) => readonly ["genesis-chat", "preview", string];
17
27
  };
28
+ /** How many messages a catch-up asks for at a time, and how many pages before it gives up and jumps to the newest. */
29
+ declare const CATCH_UP_PAGE = 200;
30
+ declare const CATCH_UP_MAX_PAGES = 10;
31
+ /** What a messages list holds in the query cache: the page, and whether older messages exist. */
32
+ type MessagesData = MessagePage;
18
33
  /** Does a messages list (`[genesis-chat, conversation, id, messages, threadRootId, topic]`) hold this message? */
19
34
  declare function belongsTo(key: readonly unknown[], m: ChatMessage): boolean;
35
+ /**
36
+ * Hosted video uploads (Mux direct uploads): your server makes an upload and returns its URL;
37
+ * the renderer PUTs the recording there and sends `{ kind: "video", provider: "mux", providerId,
38
+ * status: "pending" }`. Without it, videos go to storage as files.
39
+ */
40
+ interface ChatVideoUpload {
41
+ create(conversationId: string): Promise<{
42
+ url: string;
43
+ providerId: string;
44
+ provider: "mux";
45
+ }>;
46
+ }
47
+ /** What an app attachment kind's card shows (a form, a check-in): plain data, drawn by each renderer. */
48
+ interface ChatAttachmentKind {
49
+ title(attachment: ChatAttachment): string;
50
+ subtitle?(attachment: ChatAttachment): string | null | undefined;
51
+ /** The card's button: a link (`href`) or a handler (`onPress`). */
52
+ action?(attachment: ChatAttachment): {
53
+ label: string;
54
+ href?: string;
55
+ onPress?: () => void;
56
+ } | null | undefined;
57
+ }
20
58
  interface ChatContextValue {
21
59
  client: ChatClient;
22
60
  userId: string;
23
61
  outbox: Outbox;
24
62
  queue: QueuedMessage[];
25
63
  loadPeople?: (userIds: string[]) => Promise<Record<string, ChatPerson>>;
64
+ videoUpload?: ChatVideoUpload;
65
+ attachmentKinds?: Record<string, ChatAttachmentKind>;
66
+ /** Retries every failed message (call it when the connection comes back: AppState, NetInfo). */
67
+ retryAll(): Promise<void>;
68
+ /** Forgets every unsent message, here and in the outbox store (call it on sign-out). */
69
+ clearOutbox(): Promise<void>;
26
70
  /** Subscribe to inbox hints (one channel for the app, fanned out here). */
27
71
  onInbox(listener: (s: ChatSignal) => void): () => void;
28
72
  }
@@ -34,14 +78,22 @@ interface ChatProviderProps {
34
78
  outboxStore?: OutboxStore;
35
79
  /** Reads a native file URI for upload. */
36
80
  readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
37
- /** Replaces the plain upload (resumable uploads on native). */
38
- upload?: (conversationId: string, file: LocalFile) => Promise<string>;
81
+ /** Replaces the plain upload (resumable uploads on native). `signal` aborts when the message is discarded or the outbox cleared. */
82
+ upload?: (conversationId: string, file: LocalFile, opts?: {
83
+ signal?: AbortSignal;
84
+ }) => Promise<string>;
39
85
  /** Makes client ids (default: a random UUID that works without crypto.randomUUID). */
40
86
  newId?: () => string;
41
87
  /** Keeps a byte file (a pasted image, a recording) across launches: write it, return a URI. */
42
88
  persistFile?: (file: LocalFile) => Promise<string>;
89
+ /** Lets go of a sent file's preview (default: revokes `blob:` object URLs only). */
90
+ releasePreview?: (url: string) => void;
43
91
  /** Names and avatars for user ids, fetched in batches (usePeople, usePerson). */
44
92
  loadPeople?: (userIds: string[]) => Promise<Record<string, ChatPerson>>;
93
+ /** Hosted video uploads (Mux); videos go to storage without it. */
94
+ videoUpload?: ChatVideoUpload;
95
+ /** Cards for your own attachment kinds (`{ form: { title, subtitle, action } }`). */
96
+ attachmentKinds?: Record<string, ChatAttachmentKind>;
45
97
  children: ReactNode;
46
98
  }
47
99
  /** How the app shows a person. */
@@ -50,11 +102,13 @@ interface ChatPerson {
50
102
  avatarUrl?: string | null;
51
103
  }
52
104
  /** Puts chat in reach of every hook below it. Needs a TanStack QueryClientProvider above it. */
53
- declare function ChatProvider({ client, userId, outboxStore, readFile, upload, newId, persistFile, loadPeople, children }: ChatProviderProps): react.JSX.Element;
105
+ declare function ChatProvider({ client, userId, outboxStore, readFile, upload, newId, persistFile, releasePreview, loadPeople, videoUpload, attachmentKinds, children, }: ChatProviderProps): react.JSX.Element;
54
106
  declare function useChat(): ChatContextValue;
55
107
  /** Your conversations, live. `kind` narrows to one kind; `archived` shows the archived ones. */
56
108
  declare function useInbox(opts?: {
57
109
  kind?: string;
110
+ kinds?: string[];
111
+ excludeKinds?: string[];
58
112
  archived?: boolean;
59
113
  limit?: number;
60
114
  }): {
@@ -64,7 +118,9 @@ declare function useInbox(opts?: {
64
118
  refetch: (options?: _tanstack_query_core.RefetchOptions) => Promise<_tanstack_query_core.QueryObserverResult<NoInfer<ChatInboxItem[]>, Error>>;
65
119
  };
66
120
  /** How many messages wait for you everywhere (for a badge), live. */
67
- declare function useUnreadTotal(): number;
121
+ declare function useUnreadTotal(opts?: {
122
+ excludeKinds?: string[];
123
+ }): number;
68
124
  interface UseConversationOptions {
69
125
  /** Mark messages read as they arrive while this is true (the thread is on screen). Default true. */
70
126
  markRead?: boolean;
@@ -82,6 +138,14 @@ declare function useConversation(conversationId: string | null | undefined, opts
82
138
  canPost: boolean;
83
139
  /** Your role and posting rights are still loading: show the composer as waiting, not refused. */
84
140
  accessLoading: boolean;
141
+ /** The conversation itself is still loading (it settles to `null` when you can't see it). */
142
+ conversationLoading: boolean;
143
+ /** Your role couldn't be loaded (offline): offer `refetchAccess`, don't say "can't post". */
144
+ accessError: Error | null;
145
+ refetchAccess: (options?: _tanstack_query_core.RefetchOptions) => Promise<_tanstack_query_core.QueryObserverResult<NoInfer<{
146
+ role: ChatRole | null;
147
+ canPost: boolean;
148
+ }>, Error>>;
85
149
  /** The kind's rules (max members, threads, who posts). */
86
150
  kind: NonNullable<NoInfer<ChatKindRules | null>> | null;
87
151
  /** May you pin here: owners and admins, or anyone in a two-person conversation. */
@@ -95,11 +159,14 @@ declare function useConversation(conversationId: string | null | undefined, opts
95
159
  hasEarlier: boolean;
96
160
  loadingEarlier: boolean;
97
161
  loadEarlier: () => Promise<void>;
162
+ loadUntil: (seq: number) => Promise<boolean>;
98
163
  /** Where to draw the "New" line, from what you had read when it opened. */
99
164
  firstUnreadId: string | null;
100
165
  activity: ChatActivity[];
101
166
  signalTyping: () => void;
102
167
  signalRecording: () => void;
168
+ /** Tells the others you are recording a video (throttled). */
169
+ signalRecordingVideo: () => void;
103
170
  markRead: () => Promise<void>;
104
171
  send: (input: Omit<SendInput, "conversationId" | "clientId">, files?: LocalFile[]) => Promise<string>;
105
172
  retry: (clientId: string) => Promise<void>;
@@ -133,5 +200,80 @@ declare function useStartConversation(): (kind: string, memberIds: string[], opt
133
200
  subjectType?: string;
134
201
  subjectId?: string;
135
202
  }) => Promise<string>;
203
+ /**
204
+ * A conversation's topics, live (set_topic's hint refreshes them), and `setTopic` for owners
205
+ * and admins: add one (`setTopic("week-1", { title: "Week 1" })`), close or reopen it.
206
+ */
207
+ declare function useTopics(conversationId: string | null | undefined): {
208
+ topics: ChatTopic[];
209
+ isLoading: boolean;
210
+ error: Error | null;
211
+ setTopic: (topic: string, input?: TopicInput) => Promise<void>;
212
+ refetch: (options?: _tanstack_query_core.RefetchOptions) => Promise<_tanstack_query_core.QueryObserverResult<NoInfer<ChatTopic[]>, Error>>;
213
+ };
214
+ /**
215
+ * Your scheduled messages (in one conversation, or all), soonest first, with `schedule` and
216
+ * `cancel`. `schedule` uploads byte files first (`LocalFile.data` a Blob or ArrayBuffer; a
217
+ * native URI goes through the provider's `readFile`), then schedules them with the message.
218
+ */
219
+ declare function useScheduled(conversationId?: string | null, opts?: {
220
+ enabled?: boolean;
221
+ }): {
222
+ items: ScheduledMessage[];
223
+ isLoading: boolean;
224
+ error: Error | null;
225
+ schedule: (input: Omit<ScheduleInput, "clientId"> & {
226
+ clientId?: string;
227
+ }, files?: LocalFile[]) => Promise<string>;
228
+ cancel: (id: string) => Promise<void>;
229
+ refetch: (options?: _tanstack_query_core.RefetchOptions) => Promise<_tanstack_query_core.QueryObserverResult<NoInfer<ScheduledMessage[]>, Error>>;
230
+ };
231
+ /**
232
+ * Availability: yours (`mine`, `setMine`) and, given a conversation, its members' (`members`,
233
+ * `forUser`). Pair with `awayLine` and `isWithinWorkingHours` from the core.
234
+ */
235
+ declare function useAvailability(conversationId?: string | null): {
236
+ mine: NonNullable<NoInfer<ChatAvailability | null>> | null;
237
+ mineLoading: boolean;
238
+ setMine: (input: AvailabilityInput) => Promise<void>;
239
+ members: ChatAvailability[];
240
+ /** Everyone else's, without yours. */
241
+ others: ChatAvailability[];
242
+ forUser: (id: string) => ChatAvailability | null;
243
+ isLoading: boolean;
244
+ };
245
+ /** Sends one message into many conversations (replies come back privately in each). */
246
+ declare function useBroadcast(): {
247
+ send: (conversationIds: string[], body: string, opts?: {
248
+ silent?: boolean;
249
+ clientId?: string;
250
+ }) => Promise<string[]>;
251
+ sending: boolean;
252
+ };
253
+ /** The value, once it has stopped changing for `ms`. */
254
+ declare function useDebouncedValue<T>(value: T, ms: number): T;
255
+ /** Search, debounced (250 ms by default), in one conversation or all of yours. Queries under two characters return nothing. */
256
+ declare function useSearch(query: string, opts?: {
257
+ conversationId?: string | null;
258
+ debounceMs?: number;
259
+ limit?: number;
260
+ }): {
261
+ hits: ChatSearchHit[];
262
+ /** The query the hits are for. */
263
+ query: string;
264
+ /** Typing, or fetching: results are on their way. */
265
+ isSearching: boolean;
266
+ error: Error | null;
267
+ };
268
+ /** At most this many link-preview reads at once, per client. */
269
+ declare const LINK_PREVIEW_CONCURRENCY = 4;
270
+ /**
271
+ * Link previews for these messages, by message id, in link order (only those the server has
272
+ * fetched). Pass the messages whose text has links (`extractLinks(body).length > 0`). One query
273
+ * per message, so a new message asks only for its own, and at most LINK_PREVIEW_CONCURRENCY
274
+ * run at once. One with none yet is asked again a few times, since the runner fills them
275
+ * moments after it lands.
276
+ */
277
+ declare function useLinkPreviews(messageIds: (string | null | undefined)[]): Record<string, ChatLinkPreview[]>;
136
278
 
137
- export { type ChatPerson, ChatProvider, type ChatProviderProps, type ConversationState, LocalFile, QueuedMessage, ThreadItem, type UseConversationOptions, belongsTo, chatKeys, useChat, useConversation, useFileUrls, useInbox, useMessageActions, usePeople, usePerson, usePins, useStartConversation, useUnreadTotal };
279
+ export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, type ChatAttachmentKind, type ChatPerson, ChatProvider, type ChatProviderProps, type ChatVideoUpload, type ConversationState, LINK_PREVIEW_CONCURRENCY, LocalFile, type MessagesData, QueuedMessage, ThreadItem, type UseConversationOptions, belongsTo, chatKeys, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal };
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- export { ChatProvider, belongsTo, chatKeys, useChat, useConversation, useFileUrls, useInbox, useMessageActions, usePeople, usePerson, usePins, useStartConversation, useUnreadTotal } from './chunk-27UTR5FW.js';
2
- import './chunk-SAY24E6V.js';
3
- import './chunk-JQXZUEEL.js';
1
+ export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, ChatProvider, LINK_PREVIEW_CONCURRENCY, belongsTo, chatKeys, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal } from './chunk-PZTOY2LY.js';
2
+ import './chunk-5D5A7I4Q.js';
3
+ import './chunk-7S6HYNWW.js';
4
4
  //# sourceMappingURL=react.js.map
5
5
  //# sourceMappingURL=react.js.map