@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,544 @@
1
+ import { randomId, attachmentKindFor, ChatError } from './chunk-GBJ3YI4E.js';
2
+
3
+ // src/core/thread.ts
4
+ function mergeThread(server, pending) {
5
+ const byId = /* @__PURE__ */ new Map();
6
+ for (const m of server) byId.set(m.id, m);
7
+ const sorted = [...byId.values()].sort((a, b) => a.seq - b.seq);
8
+ const landed = new Set(sorted.map((m) => m.clientId));
9
+ const waiting = pending.filter((p) => !landed.has(p.clientId)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
10
+ return [
11
+ ...sorted.map((message) => ({ type: "message", message })),
12
+ ...waiting.map((p) => ({ type: "pending", pending: p }))
13
+ ];
14
+ }
15
+ function upsertMessages(existing, incoming) {
16
+ const byId = new Map(existing.map((m) => [m.id, m]));
17
+ for (const m of incoming) byId.set(m.id, m);
18
+ return [...byId.values()].sort((a, b) => a.seq - b.seq);
19
+ }
20
+ function itemSender(item) {
21
+ return item.type === "message" ? item.message.senderId : item.pending.senderId;
22
+ }
23
+ function itemTime(item) {
24
+ return item.type === "message" ? item.message.createdAt : item.pending.createdAt;
25
+ }
26
+ function startsGroup(items, index, gapMs = 5 * 6e4) {
27
+ if (index === 0) return true;
28
+ const prev = items[index - 1];
29
+ const cur = items[index];
30
+ if (cur.type === "message" && cur.message.kind === "system") return true;
31
+ if (prev.type === "message" && prev.message.kind === "system") return true;
32
+ if (itemSender(prev) !== itemSender(cur)) return true;
33
+ return Date.parse(itemTime(cur)) - Date.parse(itemTime(prev)) > gapMs || !sameDay(itemTime(prev), itemTime(cur));
34
+ }
35
+ function sameDay(a, b) {
36
+ const x = new Date(a);
37
+ const y = new Date(b);
38
+ return x.getFullYear() === y.getFullYear() && x.getMonth() === y.getMonth() && x.getDate() === y.getDate();
39
+ }
40
+ function startsDay(items, index) {
41
+ return index === 0 || !sameDay(itemTime(items[index - 1]), itemTime(items[index]));
42
+ }
43
+ function dayLabel(iso, now = /* @__PURE__ */ new Date(), locale) {
44
+ const d = new Date(iso);
45
+ const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
46
+ const days = Math.round((startOf(now) - startOf(d)) / 864e5);
47
+ if (days === 0) return "Today";
48
+ if (days === 1) return "Yesterday";
49
+ if (days > 1 && days < 7) return d.toLocaleDateString(locale, { weekday: "long" });
50
+ return d.toLocaleDateString(locale, { day: "numeric", month: "short", year: d.getFullYear() === now.getFullYear() ? void 0 : "numeric" });
51
+ }
52
+ function firstUnreadId(messages, lastReadSeq, me) {
53
+ return messages.find((m) => m.seq > lastReadSeq && m.senderId !== me && !m.deletedAt)?.id ?? null;
54
+ }
55
+ function summarizeReactions(message, me) {
56
+ const order = [];
57
+ const by = /* @__PURE__ */ new Map();
58
+ for (const r of message.reactions) {
59
+ if (!by.has(r.emoji)) {
60
+ by.set(r.emoji, []);
61
+ order.push(r.emoji);
62
+ }
63
+ by.get(r.emoji).push(r.userId);
64
+ }
65
+ return order.map((emoji) => ({ emoji, count: by.get(emoji).length, mine: by.get(emoji).includes(me), userIds: by.get(emoji) })).sort((a, b) => b.count - a.count || order.indexOf(a.emoji) - order.indexOf(b.emoji));
66
+ }
67
+ function currentActivity(activity, now = Date.now(), staleMs = 6e3) {
68
+ const latest = /* @__PURE__ */ new Map();
69
+ for (const a of activity) {
70
+ if (now - a.at > staleMs) continue;
71
+ const prev = latest.get(a.userId);
72
+ if (!prev || prev.at < a.at) latest.set(a.userId, a);
73
+ }
74
+ return [...latest.values()];
75
+ }
76
+ function activityLine(activity, nameOf) {
77
+ if (activity.length === 0) return null;
78
+ const recording = activity.find((a) => a.activity !== "typing");
79
+ if (recording) {
80
+ return `${nameOf(recording.userId)} is recording ${recording.activity === "recording-video" ? "a video" : "a voice message"}\u2026`;
81
+ }
82
+ const names = activity.map((a) => nameOf(a.userId));
83
+ if (names.length === 1) return `${names[0]} is typing\u2026`;
84
+ if (names.length === 2) return `${names[0]} and ${names[1]} are typing\u2026`;
85
+ return "Several people are typing\u2026";
86
+ }
87
+ function throttle(fn, everyMs = 3e3) {
88
+ let last = 0;
89
+ return () => {
90
+ const now = Date.now();
91
+ if (now - last >= everyMs) {
92
+ last = now;
93
+ fn();
94
+ }
95
+ };
96
+ }
97
+ function parseMentions(body) {
98
+ const ids = /* @__PURE__ */ new Set();
99
+ for (const m of body.matchAll(/@\[[^\]]{1,80}\]\(user:([0-9a-f-]{36})\)/gi)) ids.add(m[1].toLowerCase());
100
+ return [...ids];
101
+ }
102
+ function previewText(body, attachment, deleted = false) {
103
+ if (deleted) return "Message deleted";
104
+ const text = body.replace(/@\[([^\]]+)\]\(user:[0-9a-f-]{36}\)/gi, "@$1").replace(/[*_`#>~]/g, "").trim();
105
+ if (text) return text.length > 140 ? `${text.slice(0, 139)}\u2026` : text;
106
+ switch (attachment) {
107
+ case "image":
108
+ return "Photo";
109
+ case "voice":
110
+ return "Voice message";
111
+ case "video":
112
+ return "Video";
113
+ case "file":
114
+ return "File";
115
+ case null:
116
+ case void 0:
117
+ return "";
118
+ default:
119
+ return "Attachment";
120
+ }
121
+ }
122
+
123
+ // src/core/outbox.ts
124
+ function describe(f) {
125
+ return {
126
+ name: f.name,
127
+ contentType: f.contentType,
128
+ kind: f.meta?.kind ?? attachmentKindFor(f.contentType),
129
+ meta: f.meta,
130
+ ...f.hosted ? { hosted: true } : {}
131
+ };
132
+ }
133
+ var memoryOutboxStore = () => {
134
+ let saved = [];
135
+ return { load: async () => saved, save: async (q) => void (saved = q) };
136
+ };
137
+ function releaseObjectUrl(url) {
138
+ if (!url.startsWith("blob:") || typeof URL === "undefined" || typeof URL.revokeObjectURL !== "function") return;
139
+ try {
140
+ URL.revokeObjectURL(url);
141
+ } catch {
142
+ }
143
+ }
144
+ function createOutbox(opts) {
145
+ const store = opts.store ?? memoryOutboxStore();
146
+ let queue = [];
147
+ let generation = 0;
148
+ const running = /* @__PURE__ */ new Map();
149
+ const aborts = /* @__PURE__ */ new Map();
150
+ const uploaded = /* @__PURE__ */ new Map();
151
+ const transfers = /* @__PURE__ */ new Map();
152
+ const hostedAborts = /* @__PURE__ */ new Map();
153
+ const release = opts.releasePreview ?? releaseObjectUrl;
154
+ const releasePreviews = (q) => {
155
+ for (const url of /* @__PURE__ */ new Set([...Object.values(q.previews ?? {}), ...q.files.map((f) => f.previewUrl)])) {
156
+ if (typeof url === "string" && url) release(url);
157
+ }
158
+ };
159
+ const newId = opts.newId ?? randomId;
160
+ const saveable = (q) => {
161
+ const slots = q.pendingFiles?.length ? q.pendingFiles : q.files.map(describe);
162
+ const here = [...q.files];
163
+ const files = [];
164
+ const pendingFiles = slots.map((slot) => {
165
+ if (slot.lost) return slot;
166
+ const f = here.shift();
167
+ if (f && typeof f.data === "string") {
168
+ files.push(f);
169
+ return { ...slot, lost: false };
170
+ }
171
+ return { ...slot, lost: true };
172
+ });
173
+ return { ...q, files, pendingFiles };
174
+ };
175
+ const emit = () => {
176
+ opts.onChange?.([...queue]);
177
+ void store.save(queue.map(saveable));
178
+ };
179
+ const setProgress = (q, index, sent, total) => {
180
+ const slots = q.pendingFiles;
181
+ if (!slots?.[index] || !queue.includes(q)) return;
182
+ const progress = total > 0 ? Math.round(Math.min(1, Math.max(0, sent / total)) * 100) / 100 : 0;
183
+ if (slots[index].progress === progress) return;
184
+ q.pendingFiles = slots.map((slot, j) => j === index ? { ...slot, progress } : slot);
185
+ opts.onChange?.([...queue]);
186
+ };
187
+ const stopHosted = (clientId) => {
188
+ hostedAborts.get(clientId)?.abort();
189
+ hostedAborts.delete(clientId);
190
+ for (const k of [...transfers.keys()]) if (k.startsWith(`${clientId}:`)) transfers.delete(k);
191
+ };
192
+ async function persist(q) {
193
+ if (!opts.persistFile) return;
194
+ for (let i = 0; i < q.files.length; i++) {
195
+ const f = q.files[i];
196
+ if (typeof f.data === "string") continue;
197
+ try {
198
+ q.files[i] = { ...f, data: await opts.persistFile(f) };
199
+ } catch {
200
+ }
201
+ }
202
+ }
203
+ let loading = null;
204
+ function ensureLoaded() {
205
+ loading ??= (async () => {
206
+ const gen = generation;
207
+ let saved = [];
208
+ try {
209
+ saved = await store.load() ?? [];
210
+ } catch {
211
+ saved = [];
212
+ }
213
+ if (gen !== generation) return;
214
+ const live = new Set(queue.map((q) => q.clientId));
215
+ const restored = saved.filter((q) => !live.has(q.clientId)).map((q) => {
216
+ const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;
217
+ const rest = { ...q };
218
+ delete rest.sentMessageId;
219
+ return {
220
+ ...rest,
221
+ status: q.status === "sending" || lost > 0 ? "failed" : q.status,
222
+ error: lost > 0 ? "Attach the file again to send" : q.error ?? "Not sent",
223
+ files: q.files ?? [],
224
+ attempts: q.attempts ?? 0,
225
+ lostFiles: lost
226
+ };
227
+ });
228
+ if (restored.length) {
229
+ queue = [...restored, ...queue];
230
+ opts.onChange?.([...queue]);
231
+ }
232
+ })();
233
+ return loading;
234
+ }
235
+ async function uploadFile(conversationId, file, signal, onProgress) {
236
+ if (opts.upload) return opts.upload(conversationId, file, { signal, onProgress });
237
+ const data = typeof file.data === "string" ? await (opts.readFile ?? notReadable)(file.data) : file.data;
238
+ return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType, onProgress, signal });
239
+ }
240
+ const isHosted = (f) => Boolean(f.hosted && opts.hostedUpload);
241
+ async function startHosted(q, index, file) {
242
+ const key = `${q.clientId}:${index}`;
243
+ const known = q.hosted?.[index];
244
+ if (known && (known.uploaded || transfers.has(key))) return known;
245
+ let controller = hostedAborts.get(q.clientId);
246
+ if (!controller && typeof AbortController !== "undefined") {
247
+ controller = new AbortController();
248
+ hostedAborts.set(q.clientId, controller);
249
+ }
250
+ const started = await opts.hostedUpload(q.input.conversationId, file, {
251
+ signal: controller?.signal,
252
+ onProgress: (sent, total) => setProgress(q, index, sent, total),
253
+ ...known ? { resume: known } : {}
254
+ });
255
+ const record = { provider: started.provider, providerId: started.providerId, ...started.url ? { url: started.url } : {} };
256
+ q.hosted = { ...q.hosted, [index]: record };
257
+ if (queue.includes(q)) emit();
258
+ const transfer = Promise.resolve(started.done).then(
259
+ () => {
260
+ record.uploaded = true;
261
+ if (transfers.get(key) === transfer) transfers.delete(key);
262
+ setProgress(q, index, 1, 1);
263
+ },
264
+ (e) => {
265
+ if (transfers.get(key) === transfer) transfers.delete(key);
266
+ throw e;
267
+ }
268
+ );
269
+ transfer.catch(() => void 0);
270
+ transfers.set(key, transfer);
271
+ return record;
272
+ }
273
+ async function settleHosted(q, gen) {
274
+ const waiting = Object.entries(q.hosted ?? {}).filter(([, r]) => !r.uploaded).map(([i]) => transfers.get(`${q.clientId}:${i}`) ?? Promise.reject(new ChatError("The upload stopped", void 0, "upload")));
275
+ try {
276
+ await Promise.all(waiting);
277
+ } catch (e) {
278
+ if (gen !== generation || !queue.includes(q) || q.status !== "sending") return;
279
+ q.attempts += 1;
280
+ q.status = "failed";
281
+ q.error = e instanceof ChatError && e.code === "GSABORT" ? "The upload was cancelled" : "The video didn't finish uploading";
282
+ emit();
283
+ return;
284
+ }
285
+ if (gen !== generation || !queue.includes(q)) return;
286
+ if (Object.values(q.hosted ?? {}).some((r) => !r.uploaded)) return void settleHosted(q, gen);
287
+ queue = queue.filter((x) => x !== q);
288
+ hostedAborts.delete(q.clientId);
289
+ emit();
290
+ releasePreviews(q);
291
+ }
292
+ async function run(conversationId) {
293
+ const gen = generation;
294
+ if (running.get(conversationId) === gen) return;
295
+ running.set(conversationId, gen);
296
+ const stale = (q) => gen !== generation || !queue.includes(q);
297
+ try {
298
+ for (; ; ) {
299
+ if (gen !== generation) return;
300
+ const next = queue.find((q) => q.input.conversationId === conversationId && !q.sentMessageId);
301
+ if (!next || next.status !== "sending") break;
302
+ if ((next.lostFiles ?? 0) > 0) {
303
+ next.status = "failed";
304
+ next.error = "Attach the file again to send";
305
+ emit();
306
+ break;
307
+ }
308
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
309
+ if (controller) aborts.set(next.clientId, controller);
310
+ try {
311
+ const attachments = [...next.input.attachments ?? []];
312
+ for (let i = 0; i < next.files.length; i++) {
313
+ const file = next.files[i];
314
+ if (isHosted(file)) {
315
+ const hosted = await startHosted(next, i, file);
316
+ if (stale(next)) return;
317
+ attachments.push({
318
+ ...file.meta,
319
+ kind: file.meta?.kind ?? attachmentKindFor(file.contentType),
320
+ provider: hosted.provider,
321
+ providerId: hosted.providerId,
322
+ status: "pending",
323
+ name: file.meta?.name ?? file.name,
324
+ mime: file.contentType
325
+ });
326
+ continue;
327
+ }
328
+ const key = `${next.clientId}:${i}`;
329
+ let path = uploaded.get(key);
330
+ if (!path) {
331
+ const index = i;
332
+ path = await uploadFile(conversationId, file, controller?.signal, (sent, total) => setProgress(next, index, sent, total));
333
+ if (stale(next)) return;
334
+ uploaded.set(key, path);
335
+ }
336
+ attachments.push({
337
+ ...file.meta,
338
+ kind: file.meta?.kind ?? attachmentKindFor(file.contentType),
339
+ path,
340
+ name: file.meta?.name ?? file.name,
341
+ mime: file.contentType
342
+ });
343
+ }
344
+ if (stale(next)) return;
345
+ const message = await opts.client.send({ ...next.input, attachments });
346
+ if (gen !== generation) return;
347
+ for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);
348
+ if (Object.values(next.hosted ?? {}).some((r) => !r.uploaded)) {
349
+ next.sentMessageId = message.id;
350
+ emit();
351
+ opts.onSent?.(message, next.clientId);
352
+ void settleHosted(next, gen);
353
+ continue;
354
+ }
355
+ queue = queue.filter((q) => q.clientId !== next.clientId);
356
+ hostedAborts.delete(next.clientId);
357
+ emit();
358
+ opts.onSent?.(message, next.clientId);
359
+ releasePreviews(next);
360
+ } catch (e) {
361
+ if (stale(next)) return;
362
+ const err = e instanceof ChatError ? e : new ChatError(e instanceof Error ? e.message : String(e), void 0, "send");
363
+ next.attempts += 1;
364
+ next.status = "failed";
365
+ next.permanent = err.permanent;
366
+ next.error = err.forbidden ? "You can't post here" : err.transient ? "Not sent. Check your connection." : err.message;
367
+ emit();
368
+ break;
369
+ } finally {
370
+ if (controller && aborts.get(next.clientId) === controller) aborts.delete(next.clientId);
371
+ }
372
+ }
373
+ } finally {
374
+ if (running.get(conversationId) === gen) running.delete(conversationId);
375
+ }
376
+ }
377
+ const canAutoRetry = (x) => !x.permanent && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== "string" || Boolean(isHosted(f) ? opts.hostedUpload : opts.readFile || opts.upload));
378
+ const requeue = (x) => {
379
+ x.status = "sending";
380
+ delete x.sentMessageId;
381
+ };
382
+ return {
383
+ /** Queues a message; it shows at once and sends in order. Returns its clientId. */
384
+ async send(input, files = []) {
385
+ await ensureLoaded();
386
+ const gen = generation;
387
+ const clientId = input.clientId ?? newId();
388
+ const previews = {};
389
+ files.forEach((f, i) => {
390
+ if (f.previewUrl) previews[i] = f.previewUrl;
391
+ });
392
+ const queued = {
393
+ clientId,
394
+ input: { ...input, clientId },
395
+ senderId: opts.userId,
396
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
397
+ status: "sending",
398
+ previews,
399
+ files: [...files],
400
+ pendingFiles: files.map(describe),
401
+ attempts: 0
402
+ };
403
+ queue.push(queued);
404
+ opts.onChange?.([...queue]);
405
+ await persist(queued);
406
+ if (gen !== generation || !queue.includes(queued)) return clientId;
407
+ emit();
408
+ void run(input.conversationId);
409
+ return clientId;
410
+ },
411
+ async retry(clientId) {
412
+ await ensureLoaded();
413
+ const q = queue.find((x) => x.clientId === clientId);
414
+ if (!q) return;
415
+ for (const x of queue) {
416
+ if (x.input.conversationId !== q.input.conversationId || x.status !== "failed") continue;
417
+ if ((x.lostFiles ?? 0) > 0) break;
418
+ requeue(x);
419
+ x.permanent = false;
420
+ }
421
+ emit();
422
+ void run(q.input.conversationId);
423
+ },
424
+ /**
425
+ * Gives a message the files it lost on a relaunch and sends it. `files` fill the lost
426
+ * slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass
427
+ * one per lost slot; with fewer, the message keeps waiting for the rest.
428
+ */
429
+ async reattach(clientId, files) {
430
+ await ensureLoaded();
431
+ const q = queue.find((x) => x.clientId === clientId);
432
+ if (!q) return;
433
+ const slots = q.pendingFiles ?? [];
434
+ const kept = [...q.files];
435
+ const given = [...files];
436
+ const merged = [];
437
+ const described = [];
438
+ for (const slot of slots) {
439
+ if (slot.lost) {
440
+ const f = given.shift();
441
+ if (f) {
442
+ merged.push(f);
443
+ described.push(describe(f));
444
+ } else {
445
+ described.push(slot);
446
+ }
447
+ } else {
448
+ const f = kept.shift();
449
+ if (f) {
450
+ merged.push(f);
451
+ described.push(slot);
452
+ }
453
+ }
454
+ }
455
+ if (slots.length === 0) {
456
+ merged.push(...given);
457
+ described.push(...given.map(describe));
458
+ }
459
+ q.files = merged;
460
+ q.pendingFiles = described;
461
+ q.lostFiles = described.filter((d) => d.lost).length;
462
+ if (q.lostFiles > 0) {
463
+ emit();
464
+ return;
465
+ }
466
+ const gen = generation;
467
+ await persist(q);
468
+ if (gen !== generation || !queue.includes(q)) return;
469
+ requeue(q);
470
+ emit();
471
+ void run(q.input.conversationId);
472
+ },
473
+ /**
474
+ * Retries failed messages (after the connection comes back), in order per conversation,
475
+ * stopping at the first that can't go on its own: one waiting for a lost file, or one the
476
+ * server refused (`permanent`, retried only by hand). The ones after it keep waiting.
477
+ */
478
+ async retryAll() {
479
+ await ensureLoaded();
480
+ const conversations = /* @__PURE__ */ new Set();
481
+ const held = /* @__PURE__ */ new Set();
482
+ for (const x of queue) {
483
+ const c = x.input.conversationId;
484
+ if (held.has(c) || x.status !== "failed") continue;
485
+ if (!canAutoRetry(x)) {
486
+ if (!x.sentMessageId) held.add(c);
487
+ continue;
488
+ }
489
+ requeue(x);
490
+ conversations.add(c);
491
+ }
492
+ emit();
493
+ for (const c of conversations) void run(c);
494
+ },
495
+ /** Forgets every queued message, here and in the store (call it on sign-out). */
496
+ async clear() {
497
+ generation += 1;
498
+ for (const c of aborts.values()) c.abort();
499
+ aborts.clear();
500
+ for (const c of hostedAborts.values()) c.abort();
501
+ hostedAborts.clear();
502
+ transfers.clear();
503
+ running.clear();
504
+ const gone = queue;
505
+ queue = [];
506
+ uploaded.clear();
507
+ gone.forEach(releasePreviews);
508
+ opts.onChange?.([]);
509
+ await ensureLoaded();
510
+ await store.save([]);
511
+ },
512
+ async discard(clientId) {
513
+ await ensureLoaded();
514
+ const gone = queue.find((q) => q.clientId === clientId);
515
+ queue = queue.filter((q) => q.clientId !== clientId);
516
+ aborts.get(clientId)?.abort();
517
+ aborts.delete(clientId);
518
+ stopHosted(clientId);
519
+ if (gone) releasePreviews(gone);
520
+ emit();
521
+ },
522
+ /**
523
+ * Uploads one file to storage the way the outbox does (the `upload` hook, or `readFile` then
524
+ * the client's upload) and resolves with its path: for files sent outside the queue, such as
525
+ * a scheduled message's.
526
+ */
527
+ uploadFile(conversationId, file, o = {}) {
528
+ return uploadFile(conversationId, file, o.signal, o.onProgress);
529
+ },
530
+ pending(conversationId) {
531
+ return queue.filter((q) => !conversationId || q.input.conversationId === conversationId).map((q) => ({ ...q, files: q.pendingFiles }));
532
+ },
533
+ /** A new client id, made the way this outbox makes them. */
534
+ newId,
535
+ load: ensureLoaded
536
+ };
537
+ }
538
+ async function notReadable() {
539
+ throw new ChatError("This file can't be read on this device", "GSFILE", "upload");
540
+ }
541
+
542
+ export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, releaseObjectUrl, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages };
543
+ //# sourceMappingURL=chunk-A4YJX7KT.js.map
544
+ //# sourceMappingURL=chunk-A4YJX7KT.js.map