@marktiderman/genesis-chat 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +379 -13
  2. package/dist/chunk-A4YJX7KT.js +544 -0
  3. package/dist/chunk-A4YJX7KT.js.map +1 -0
  4. package/dist/chunk-AZS3HNLM.js +30 -0
  5. package/dist/chunk-AZS3HNLM.js.map +1 -0
  6. package/dist/chunk-BJCE5O7V.js +70 -0
  7. package/dist/chunk-BJCE5O7V.js.map +1 -0
  8. package/dist/chunk-GBJ3YI4E.js +966 -0
  9. package/dist/chunk-GBJ3YI4E.js.map +1 -0
  10. package/dist/chunk-JU2FAPBI.js +609 -0
  11. package/dist/chunk-JU2FAPBI.js.map +1 -0
  12. package/dist/chunk-VEPZROQB.js +187 -0
  13. package/dist/chunk-VEPZROQB.js.map +1 -0
  14. package/dist/{client-BtdALFCx.d.ts → client-DUIG1hum.d.ts} +187 -11
  15. package/dist/index.d.ts +78 -163
  16. package/dist/index.js +3 -2
  17. package/dist/native.d.ts +1939 -1
  18. package/dist/native.js +5023 -0
  19. package/dist/native.js.map +1 -1
  20. package/dist/outbox-B9NAWQ58.d.ts +219 -0
  21. package/dist/push.d.ts +206 -0
  22. package/dist/push.js +3 -0
  23. package/dist/push.js.map +1 -0
  24. package/dist/react.d.ts +176 -7
  25. package/dist/react.js +3 -3
  26. package/dist/server.d.ts +551 -21
  27. package/dist/server.js +976 -34
  28. package/dist/server.js.map +1 -1
  29. package/dist/thread-oPra7pkW.d.ts +57 -0
  30. package/dist/web.d.ts +335 -12
  31. package/dist/web.js +1784 -64
  32. package/dist/web.js.map +1 -1
  33. package/migrations/0002_genesis_chat_coaching.sql +934 -0
  34. package/migrations/0003_genesis_chat_search_threads.sql +85 -0
  35. package/migrations/tests/0002_genesis_chat_coaching.test.sql +269 -0
  36. package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
  37. package/package.json +78 -6
  38. package/src/__tests__/coaching-core.test.ts +334 -0
  39. package/src/__tests__/coaching-react.test.tsx +319 -0
  40. package/src/__tests__/coaching-web.test.tsx +521 -0
  41. package/src/__tests__/core.test.ts +91 -0
  42. package/src/__tests__/fake-xhr.ts +54 -0
  43. package/src/__tests__/outbox-hosted.test.ts +269 -0
  44. package/src/__tests__/outbox.test.ts +122 -0
  45. package/src/__tests__/realtime.test.ts +70 -0
  46. package/src/__tests__/review-279.test.ts +41 -0
  47. package/src/__tests__/review-fixes-core.test.ts +193 -0
  48. package/src/__tests__/review-fixes-web.test.tsx +405 -0
  49. package/src/__tests__/server-effects.test.ts +158 -0
  50. package/src/__tests__/server-fake-video.test.ts +81 -0
  51. package/src/__tests__/server-fakes.ts +96 -0
  52. package/src/__tests__/server-integrations.test.ts +247 -0
  53. package/src/__tests__/server-providers.test.ts +246 -0
  54. package/src/__tests__/server-runner.test.ts +277 -0
  55. package/src/__tests__/server-safety.test.ts +303 -0
  56. package/src/__tests__/web-markdown.test.tsx +88 -0
  57. package/src/__tests__/web.test.tsx +349 -0
  58. package/src/core/availability.ts +223 -0
  59. package/src/core/client.ts +783 -0
  60. package/src/core/extras.ts +106 -0
  61. package/src/core/media.ts +144 -0
  62. package/src/core/outbox.ts +635 -0
  63. package/src/core/thread.ts +171 -0
  64. package/src/core/transfer.ts +71 -0
  65. package/src/core/types.ts +328 -0
  66. package/src/index.ts +14 -0
  67. package/src/native/__tests__/auto-retry.test.tsx +128 -0
  68. package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
  69. package/src/native/__tests__/composer-state.test.ts +188 -0
  70. package/src/native/__tests__/composer.test.tsx +133 -0
  71. package/src/native/__tests__/emoji.test.tsx +254 -0
  72. package/src/native/__tests__/inbox-screen.test.tsx +48 -0
  73. package/src/native/__tests__/message-bubble.test.tsx +127 -0
  74. package/src/native/__tests__/organise-logic.test.ts +164 -0
  75. package/src/native/__tests__/organise-screens.test.tsx +424 -0
  76. package/src/native/__tests__/photo-viewer.test.tsx +226 -0
  77. package/src/native/__tests__/push-payload.test.ts +66 -0
  78. package/src/native/__tests__/push.test.ts +209 -0
  79. package/src/native/__tests__/recorder-machine.test.ts +96 -0
  80. package/src/native/__tests__/rows-actions.test.ts +76 -0
  81. package/src/native/__tests__/thread-layout.test.ts +41 -0
  82. package/src/native/__tests__/thread-screen.test.tsx +116 -0
  83. package/src/native/__tests__/tus.test.ts +154 -0
  84. package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
  85. package/src/native/__tests__/video-media.test.tsx +498 -0
  86. package/src/native/__tests__/video-recorder-state.test.ts +87 -0
  87. package/src/native/__tests__/video-upload.test.ts +227 -0
  88. package/src/native/access-error.tsx +31 -0
  89. package/src/native/actions.ts +42 -0
  90. package/src/native/attachment-cards.tsx +132 -0
  91. package/src/native/auto-retry.ts +69 -0
  92. package/src/native/availability.tsx +376 -0
  93. package/src/native/broadcast-sheet.tsx +193 -0
  94. package/src/native/composer-state.ts +302 -0
  95. package/src/native/composer.tsx +453 -0
  96. package/src/native/context.tsx +70 -0
  97. package/src/native/copy.ts +296 -0
  98. package/src/native/emoji-data.ts +21 -0
  99. package/src/native/emoji-picker.tsx +229 -0
  100. package/src/native/emoji.ts +118 -0
  101. package/src/native/icon-button.tsx +40 -0
  102. package/src/native/inbox-screen.tsx +122 -0
  103. package/src/native/index.ts +184 -0
  104. package/src/native/link-preview.tsx +104 -0
  105. package/src/native/media-colors.ts +15 -0
  106. package/src/native/media.tsx +187 -0
  107. package/src/native/message-actions-sheet.tsx +126 -0
  108. package/src/native/message-bubble.tsx +366 -0
  109. package/src/native/moderation.tsx +42 -0
  110. package/src/native/modules.ts +277 -0
  111. package/src/native/organise-copy.ts +195 -0
  112. package/src/native/photo-viewer.tsx +531 -0
  113. package/src/native/push.ts +375 -0
  114. package/src/native/recorder-machine.ts +176 -0
  115. package/src/native/rows.ts +81 -0
  116. package/src/native/schedule-sheet.tsx +328 -0
  117. package/src/native/search-screen.tsx +189 -0
  118. package/src/native/stack-options.ts +36 -0
  119. package/src/native/start-sheet.tsx +87 -0
  120. package/src/native/thread-screen.tsx +647 -0
  121. package/src/native/topics.tsx +234 -0
  122. package/src/native/tus.ts +282 -0
  123. package/src/native/use-composer.ts +71 -0
  124. package/src/native/use-jump.ts +91 -0
  125. package/src/native/use-voice-recorder.ts +234 -0
  126. package/src/native/video-player.tsx +410 -0
  127. package/src/native/video-recorder-state.ts +140 -0
  128. package/src/native/video-recorder.tsx +274 -0
  129. package/src/native/video-upload-tray.tsx +77 -0
  130. package/src/native/video-upload.ts +432 -0
  131. package/src/native/voice-player.tsx +301 -0
  132. package/src/react/index.tsx +894 -0
  133. package/src/react/previews.ts +51 -0
  134. package/src/server/assistant.ts +235 -0
  135. package/src/server/crypto.ts +86 -0
  136. package/src/server/expo.ts +119 -0
  137. package/src/server/fake-video.ts +171 -0
  138. package/src/server/handler.ts +32 -0
  139. package/src/server/index.ts +58 -0
  140. package/src/server/moderation.ts +155 -0
  141. package/src/server/mux.ts +290 -0
  142. package/src/server/notifications.ts +130 -0
  143. package/src/server/runner.ts +468 -0
  144. package/src/server/transcribers.ts +111 -0
  145. package/src/server/types.ts +171 -0
  146. package/src/server/unfurl.ts +480 -0
  147. package/src/server/upload.ts +158 -0
  148. package/src/web/attachments.tsx +379 -0
  149. package/src/web/availability.tsx +299 -0
  150. package/src/web/broadcast.tsx +166 -0
  151. package/src/web/bubble.tsx +585 -0
  152. package/src/web/cards.tsx +136 -0
  153. package/src/web/composer.tsx +571 -0
  154. package/src/web/emoji-picker.tsx +84 -0
  155. package/src/web/error-boundary.tsx +33 -0
  156. package/src/web/format.ts +55 -0
  157. package/src/web/inbox.tsx +196 -0
  158. package/src/web/index.ts +75 -0
  159. package/src/web/markdown.tsx +245 -0
  160. package/src/web/memory-client.ts +714 -0
  161. package/src/web/panel.tsx +135 -0
  162. package/src/web/schedule.tsx +204 -0
  163. package/src/web/search.tsx +129 -0
  164. package/src/web/thread-context.ts +64 -0
  165. package/src/web/thread.tsx +830 -0
  166. package/src/web/topics.tsx +160 -0
  167. package/src/web/use-voice-recorder.ts +264 -0
  168. package/src/web/video.tsx +516 -0
  169. package/src/web/voice-note.tsx +197 -0
  170. package/dist/chunk-27UTR5FW.js +0 -322
  171. package/dist/chunk-27UTR5FW.js.map +0 -1
  172. package/dist/chunk-JQXZUEEL.js +0 -525
  173. package/dist/chunk-JQXZUEEL.js.map +0 -1
  174. package/dist/chunk-SAY24E6V.js +0 -309
  175. package/dist/chunk-SAY24E6V.js.map +0 -1
@@ -0,0 +1,219 @@
1
+ import { b as AttachmentKind, A as AttachmentInput, U as UploadProgress, H as HostedAttachment, i as ChatClient, B as PendingMessage, P as PendingFile, q as ChatMessage, F as SendInput } from './client-DUIG1hum.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
+ * A hosted video (`LocalFile.hosted`, with `hostedUpload`) doesn't wait for its bytes: the
67
+ * upload is started (the provider's upload id minted), the message is sent in its place in the
68
+ * order with the attachment `pending`, and the bytes carry on going up; the provider's webhook
69
+ * marks it ready. The message stays here (hidden from the thread once the server has it) until
70
+ * its bytes are up, so a relaunch carries the upload on (`resume`) and sends the message again
71
+ * with the same clientId, which posts nothing twice.
72
+ */
73
+
74
+ /** A file still on the device, uploaded when its message is sent. */
75
+ interface LocalFile {
76
+ /** The bytes, or (native) a file URI the platform reads in `readFile`. */
77
+ data: Blob | ArrayBuffer | string;
78
+ name: string;
79
+ contentType: string;
80
+ /** A preview to show while it uploads (an object URL, a file URI). The outbox owns it from here: see `releasePreview`. */
81
+ previewUrl?: string;
82
+ /** Known up front: a voice note's duration and waveform, a photo's size. */
83
+ meta?: Partial<Omit<AttachmentInput, "path" | "kind">> & {
84
+ kind?: AttachmentInput["kind"];
85
+ };
86
+ /**
87
+ * Send it to the hosted video provider (the outbox's `hostedUpload`) instead of storage: the
88
+ * message goes at once, in order, with the attachment pending, while the bytes go up. Without
89
+ * a `hostedUpload` it goes to storage like any file.
90
+ */
91
+ hosted?: boolean;
92
+ }
93
+ /** A hosted upload as the outbox keeps it (saved with the message), so it can be carried on. */
94
+ interface HostedUploadRecord {
95
+ provider: HostedAttachment["provider"];
96
+ providerId: string;
97
+ /** Where the bytes go, when the hook returned it: what a resume needs. */
98
+ url?: string;
99
+ /** Every byte is there. */
100
+ uploaded?: boolean;
101
+ }
102
+ /** What `hostedUpload` resolves with as soon as the upload exists (before the bytes are up). */
103
+ interface HostedUploadStart extends HostedAttachment {
104
+ /** Where the bytes go; kept with the message, handed back as `resume.url`. */
105
+ url?: string;
106
+ /** Settles when every byte is there (rejects when the transfer fails). Absent: already there. */
107
+ done?: Promise<void>;
108
+ }
109
+ interface HostedUploadOptions {
110
+ /** Aborts when the message is discarded or the outbox cleared. */
111
+ signal?: AbortSignal;
112
+ onProgress?: UploadProgress;
113
+ /**
114
+ * The upload started before (a relaunch, a retry after the transfer failed): carry it on and
115
+ * resolve with the same `providerId`. A message may already be posted with it, so mint a new
116
+ * one only when the old upload is gone; that message's video then stays pending.
117
+ */
118
+ resume?: HostedUploadRecord;
119
+ }
120
+ /** Starts (or carries on) a hosted upload; resolves once the provider's upload id exists. */
121
+ type HostedUpload = (conversationId: string, file: LocalFile, opts: HostedUploadOptions) => Promise<HostedUploadStart>;
122
+ /** Uploads a file to storage and resolves with its path. */
123
+ type StorageUpload = (conversationId: string, file: LocalFile, opts?: {
124
+ signal?: AbortSignal;
125
+ onProgress?: UploadProgress;
126
+ }) => Promise<string>;
127
+ interface QueuedMessage extends Omit<PendingMessage, "files"> {
128
+ files: LocalFile[];
129
+ /** What each file is, kept with the saved copy (PendingMessage.files). */
130
+ pendingFiles?: PendingFile[];
131
+ /** Files lost on a relaunch (bytes that weren't persisted). */
132
+ lostFiles?: number;
133
+ attempts: number;
134
+ /** Hosted uploads started, by file slot. */
135
+ hosted?: Record<number, HostedUploadRecord>;
136
+ /** Posted (this message's id): it waits here only for its hosted bytes to finish. */
137
+ sentMessageId?: string;
138
+ }
139
+ interface OutboxStore {
140
+ load(): Promise<QueuedMessage[]>;
141
+ save(queue: QueuedMessage[]): Promise<void>;
142
+ }
143
+ declare const memoryOutboxStore: () => OutboxStore;
144
+ interface OutboxOptions {
145
+ client: ChatClient;
146
+ userId: string;
147
+ store?: OutboxStore;
148
+ /** Reads a native file URI into bytes for upload. */
149
+ readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
150
+ /**
151
+ * Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts
152
+ * when the message is discarded or the outbox cleared; call `onProgress(sent, total)` as bytes
153
+ * go, and the pending bubble shows it (`PendingMessage.files[i].progress`).
154
+ */
155
+ upload?: StorageUpload;
156
+ /**
157
+ * Starts a hosted video upload for a file marked `hosted` and resolves once the provider's
158
+ * upload id exists, with `done` for the bytes. See HostedUpload; without it, hosted files go
159
+ * to storage.
160
+ */
161
+ hostedUpload?: HostedUpload;
162
+ /** Called with every change, for the UI. */
163
+ onChange?: (queue: QueuedMessage[]) => void;
164
+ /** Called when a message lands. */
165
+ onSent?: (message: ChatMessage, clientId: string) => void;
166
+ /** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */
167
+ newId?: () => string;
168
+ /**
169
+ * Keeps a file held as bytes (a pasted image, a recording) between launches: write it
170
+ * somewhere and return a URI `readFile` can read (native: the app's cache directory).
171
+ * Without it, such a file isn't saved, and after a relaunch its message asks for it again.
172
+ */
173
+ persistFile?: (file: LocalFile) => Promise<string>;
174
+ /**
175
+ * Lets go of a file's `previewUrl` once its message has landed, been discarded or cleared.
176
+ * Default: revokes `blob:` object URLs (the web's), and leaves everything else alone.
177
+ */
178
+ releasePreview?: (url: string) => void;
179
+ }
180
+ /** Revokes a `blob:` object URL; anything else (a native file URI) is left alone. */
181
+ declare function releaseObjectUrl(url: string): void;
182
+ declare function createOutbox(opts: OutboxOptions): {
183
+ /** Queues a message; it shows at once and sends in order. Returns its clientId. */
184
+ send(input: Omit<SendInput, "clientId"> & {
185
+ clientId?: string;
186
+ }, files?: LocalFile[]): Promise<string>;
187
+ retry(clientId: string): Promise<void>;
188
+ /**
189
+ * Gives a message the files it lost on a relaunch and sends it. `files` fill the lost
190
+ * slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass
191
+ * one per lost slot; with fewer, the message keeps waiting for the rest.
192
+ */
193
+ reattach(clientId: string, files: LocalFile[]): Promise<void>;
194
+ /**
195
+ * Retries failed messages (after the connection comes back), in order per conversation,
196
+ * stopping at the first that can't go on its own: one waiting for a lost file, or one the
197
+ * server refused (`permanent`, retried only by hand). The ones after it keep waiting.
198
+ */
199
+ retryAll(): Promise<void>;
200
+ /** Forgets every queued message, here and in the store (call it on sign-out). */
201
+ clear(): Promise<void>;
202
+ discard(clientId: string): Promise<void>;
203
+ /**
204
+ * Uploads one file to storage the way the outbox does (the `upload` hook, or `readFile` then
205
+ * the client's upload) and resolves with its path: for files sent outside the queue, such as
206
+ * a scheduled message's.
207
+ */
208
+ uploadFile(conversationId: string, file: LocalFile, o?: {
209
+ signal?: AbortSignal;
210
+ onProgress?: UploadProgress;
211
+ }): Promise<string>;
212
+ pending(conversationId?: string): PendingMessage[];
213
+ /** A new client id, made the way this outbox makes them. */
214
+ newId: typeof randomId;
215
+ load: () => Promise<void>;
216
+ };
217
+ type Outbox = ReturnType<typeof createOutbox>;
218
+
219
+ export { ALLOWED_EXTENSIONS as A, CHAT_LIMITS as C, type HostedUpload as H, type LocalFile as L, type Outbox as O, PLAYBACK_RATES as P, type QueuedMessage as Q, type StorageUpload as S, type HostedUploadOptions as a, type HostedUploadRecord as b, type HostedUploadStart as c, type OutboxOptions as d, type OutboxStore as e, attachmentKindFor as f, createOutbox as g, fileExtension as h, formatDuration as i, isAllowedExtension as j, recordingLimitMs as k, releaseObjectUrl as l, memoryOutboxStore as m, nextPlaybackRate as n, waveformPeaks as o, randomId as r, waveformFromMetering as w };
package/dist/push.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { i as ChatClient } from './client-DUIG1hum.js';
2
+ import { O as Outbox, e as OutboxStore, Q as QueuedMessage } from './outbox-B9NAWQ58.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"}