@marktiderman/genesis-chat 0.2.0 → 0.2.2
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.
- package/README.md +82 -7
- package/dist/{chunk-5D5A7I4Q.js → chunk-A4YJX7KT.js} +130 -16
- package/dist/chunk-A4YJX7KT.js.map +1 -0
- package/dist/chunk-AZS3HNLM.js +30 -0
- package/dist/chunk-AZS3HNLM.js.map +1 -0
- package/dist/{chunk-7S6HYNWW.js → chunk-GBJ3YI4E.js} +72 -5
- package/dist/chunk-GBJ3YI4E.js.map +1 -0
- package/dist/{chunk-PZTOY2LY.js → chunk-JU2FAPBI.js} +46 -13
- package/dist/chunk-JU2FAPBI.js.map +1 -0
- package/dist/{client-DMa2j7kz.d.ts → client-DUIG1hum.d.ts} +36 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/native.d.ts +36 -7
- package/dist/native.js +239 -116
- package/dist/native.js.map +1 -1
- package/dist/{outbox-B0y3phE0.d.ts → outbox-B9NAWQ58.d.ts} +74 -6
- package/dist/push.d.ts +2 -2
- package/dist/react.d.ts +48 -21
- package/dist/react.js +3 -3
- package/dist/server.d.ts +43 -2
- package/dist/server.js +115 -15
- package/dist/server.js.map +1 -1
- package/dist/{thread-DWXHKPdw.d.ts → thread-oPra7pkW.d.ts} +1 -1
- package/dist/web.d.ts +31 -11
- package/dist/web.js +97 -48
- package/dist/web.js.map +1 -1
- package/migrations/0003_genesis_chat_search_threads.sql +85 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +7 -3
- package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
- package/package.json +13 -9
- package/src/__tests__/coaching-core.test.ts +3 -1
- package/src/__tests__/coaching-react.test.tsx +73 -2
- package/src/__tests__/coaching-web.test.tsx +78 -6
- package/src/__tests__/fake-xhr.ts +54 -0
- package/src/__tests__/outbox-hosted.test.ts +269 -0
- package/src/__tests__/review-279.test.ts +41 -0
- package/src/__tests__/review-fixes-web.test.tsx +13 -10
- package/src/__tests__/server-fake-video.test.ts +81 -0
- package/src/__tests__/server-fakes.ts +7 -1
- package/src/__tests__/server-safety.test.ts +17 -4
- package/src/__tests__/web.test.tsx +16 -0
- package/src/core/client.ts +28 -2
- package/src/core/outbox.ts +222 -16
- package/src/core/transfer.ts +71 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +1 -0
- package/src/native/__tests__/emoji.test.tsx +16 -0
- package/src/native/__tests__/organise-screens.test.tsx +80 -0
- package/src/native/__tests__/video-media.test.tsx +81 -4
- package/src/native/attachment-cards.tsx +22 -6
- package/src/native/inbox-screen.tsx +12 -6
- package/src/native/index.ts +2 -2
- package/src/native/message-actions-sheet.tsx +10 -5
- package/src/native/message-bubble.tsx +3 -11
- package/src/native/modules.ts +2 -0
- package/src/native/schedule-sheet.tsx +23 -9
- package/src/native/search-screen.tsx +26 -5
- package/src/native/thread-screen.tsx +30 -3
- package/src/native/topics.tsx +70 -15
- package/src/native/video-player.tsx +46 -6
- package/src/native/video-recorder.tsx +6 -3
- package/src/native/voice-player.tsx +18 -0
- package/src/react/index.tsx +103 -19
- package/src/react/previews.ts +51 -0
- package/src/server/fake-video.ts +171 -0
- package/src/server/index.ts +1 -0
- package/src/web/attachments.tsx +6 -6
- package/src/web/bubble.tsx +22 -0
- package/src/web/inbox.tsx +7 -6
- package/src/web/memory-client.ts +11 -3
- package/src/web/panel.tsx +2 -2
- package/src/web/schedule.tsx +14 -2
- package/src/web/thread.tsx +36 -8
- package/src/web/video.tsx +28 -34
- package/dist/chunk-5D5A7I4Q.js.map +0 -1
- package/dist/chunk-7S6HYNWW.js.map +0 -1
- package/dist/chunk-PZTOY2LY.js.map +0 -1
package/README.md
CHANGED
|
@@ -107,8 +107,13 @@ chat.setAvailability({ awayMessage?, awayUntil?, timeZone?, hours? }): Promise<v
|
|
|
107
107
|
chat.broadcast(conversationIds, clientId, body, { silent? }): Promise<string[]>
|
|
108
108
|
|
|
109
109
|
// Search: text and transcripts, newest first; snippets mark matches with « ».
|
|
110
|
+
// A hit that is a reply in a thread has `threadRootId`: open that thread to show it.
|
|
110
111
|
chat.search(query, conversationId?, { limit? }): Promise<ChatSearchHit[]>
|
|
111
112
|
|
|
113
|
+
// Storage upload; with onProgress, bytes go to a signed upload URL through XMLHttpRequest
|
|
114
|
+
// (browsers, React Native) so progress is reported; elsewhere it reports once, at the end.
|
|
115
|
+
chat.upload(conversationId, userId, bytes, { name, contentType, onProgress?(sent, total), signal? }): Promise<string>
|
|
116
|
+
|
|
112
117
|
// Link previews the runner fetched (status ok only), by message id, through link_previews_for
|
|
113
118
|
// (one call per message, six at a time): only for messages you can read. The table is closed.
|
|
114
119
|
chat.linkPreviewsFor(messageIds): Promise<Record<string, ChatLinkPreview[]>>
|
|
@@ -132,7 +137,7 @@ const line = awayLine(await chat.availabilityFor(id).then((a) => a[0]), "Mark");
|
|
|
132
137
|
```tsx
|
|
133
138
|
const { topics, setTopic } = useTopics(conversationId); // live: set_topic's hint refreshes it
|
|
134
139
|
const thread = useConversation(conversationId, { topic: "week-1" }); // one topic; sends go into it
|
|
135
|
-
const { items, schedule, cancel } = useScheduled(conversationId); // schedule(input, files?) uploads
|
|
140
|
+
const { items, schedule, cancel } = useScheduled(conversationId); // schedule(input, files?) uploads files first, as send does; cancel also removes a failed one
|
|
136
141
|
const { mine, setMine, members, others, forUser } = useAvailability(conversationId);
|
|
137
142
|
const { send, sending } = useBroadcast(); // send(ids, body) -> message ids
|
|
138
143
|
const { hits, isSearching, query } = useSearch(text, { conversationId, debounceMs: 250 });
|
|
@@ -158,15 +163,58 @@ afterwards, so a send can't go out under the next account. The outbox owns a fil
|
|
|
158
163
|
once queued and lets go of it (`releasePreview`, default: revoke `blob:` URLs) when the message
|
|
159
164
|
lands or is discarded.
|
|
160
165
|
|
|
166
|
+
A pending message's files carry their upload progress: `PendingMessage.files[i].progress` (0 to 1),
|
|
167
|
+
reported by `client.upload`'s `onProgress` or by your `upload` prop calling `opts.onProgress(sent,
|
|
168
|
+
total)`. The web bubble draws it as a bar.
|
|
169
|
+
|
|
170
|
+
### Hosted video in the outbox
|
|
171
|
+
|
|
172
|
+
A file marked `hosted: true` goes to the hosted video provider instead of storage, through the
|
|
173
|
+
outbox, in its place in the order:
|
|
174
|
+
|
|
175
|
+
1. The outbox calls `hostedUpload(conversationId, file, { signal, onProgress, resume? })`, which
|
|
176
|
+
mints the upload and resolves at once with `{ provider: "mux" | "cloudflare", providerId,
|
|
177
|
+
status: "pending", url?, done? }`; `done` settles when the bytes are up.
|
|
178
|
+
2. The message is sent straight away with `{ kind: "video", provider, providerId, status:
|
|
179
|
+
"pending" }`, so text written after it lands after it. The provider's webhook flips it ready.
|
|
180
|
+
3. The message stays in the outbox (hidden from the thread, which has the server's copy) until
|
|
181
|
+
`done` settles, with the upload saved as `QueuedMessage.hosted[i]` (`provider`, `providerId`,
|
|
182
|
+
`url`). After a relaunch the outbox calls `hostedUpload` again with `resume` set to that record,
|
|
183
|
+
then sends the message again with the same clientId (posting nothing twice). A failed transfer
|
|
184
|
+
marks it failed without holding later messages; Retry (or `retryAll`) resumes it the same way.
|
|
185
|
+
|
|
186
|
+
On the web, `ChatProvider videoUpload` gives the default `hostedUpload` (`hostedUploadFrom`: one PUT
|
|
187
|
+
of the whole file, with progress) and the composer's video recorder sends `hosted` files. Without
|
|
188
|
+
`videoUpload` (or `hostedUpload`), hosted files go to storage like any other.
|
|
189
|
+
|
|
190
|
+
**Native contract.** Pass your own `hostedUpload` to `ChatProvider` (it wins over `videoUpload`),
|
|
191
|
+
and send recordings through `useChat().outbox.send(input, [{ data: uri, name, contentType, hosted:
|
|
192
|
+
true, meta: { kind: "video", durationMs } }])` instead of uploading first:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
hostedUpload={async (conversationId, file, { signal, onProgress, resume }) => {
|
|
196
|
+
// Resuming (a relaunch, a retry): keep the providerId; the message may already carry it.
|
|
197
|
+
const minted = resume?.url ? { url: resume.url, providerId: resume.providerId, provider: resume.provider }
|
|
198
|
+
: await videoUpload.create(conversationId);
|
|
199
|
+
const done = putChunks(minted.url, file.data as string, { signal, onProgress, resume: Boolean(resume) });
|
|
200
|
+
return { provider: minted.provider, providerId: minted.providerId, status: "pending", url: minted.url, done };
|
|
201
|
+
}}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Resolve as soon as the provider id exists; never wait for the bytes. On `resume`, carry the same
|
|
205
|
+
upload on (ask the server how much it has) and return the same `providerId`. Mint a new one only
|
|
206
|
+
when the old upload is gone: a message already posted keeps the old id, and its video stays
|
|
207
|
+
pending. Persist the file (`persistFile`) so a relaunch still has its bytes.
|
|
208
|
+
|
|
161
209
|
`ChatProvider` takes two more props:
|
|
162
210
|
|
|
163
211
|
```tsx
|
|
164
212
|
<ChatProvider
|
|
165
213
|
client={chat}
|
|
166
214
|
userId={user.id}
|
|
167
|
-
// Mux direct uploads: your server makes the upload; the composer
|
|
168
|
-
//
|
|
169
|
-
// upload to storage
|
|
215
|
+
// Mux direct uploads: your server makes the upload; the composer's recording goes through the
|
|
216
|
+
// outbox as a hosted file, sent at once as { kind: "video", provider: "mux", providerId,
|
|
217
|
+
// status: "pending" } while its bytes are PUT there. Without it, videos upload to storage.
|
|
170
218
|
// Pairs with createVideoUploadHandler from /server, which mints the upload and its passthrough;
|
|
171
219
|
// the client sends only the upload id (no attachment id).
|
|
172
220
|
videoUpload={{
|
|
@@ -212,8 +260,8 @@ generic card. Errors go to the console.
|
|
|
212
260
|
the conversation has topics (`showTopics` shows it before the first, for owners). Owners and
|
|
213
261
|
admins get "New topic" and close or reopen. In a closed topic members see "This topic is closed".
|
|
214
262
|
- **Scheduled send**: the arrow beside Send offers tomorrow 9 AM, Monday 9 AM, or a date and time;
|
|
215
|
-
"N scheduled messages" above the box opens `ChatScheduledList` with Cancel
|
|
216
|
-
turns it off). `ChatScheduledList` also works on its own, for all conversations.
|
|
263
|
+
"N scheduled messages" above the box opens `ChatScheduledList` with Cancel, or Remove for one that
|
|
264
|
+
failed when due (`scheduling={false}` turns it off). `ChatScheduledList` also works on its own, for all conversations.
|
|
217
265
|
- **Availability**: `ChatAwayBanner` above the messages when the other person is away or outside
|
|
218
266
|
their hours, or in a group everyone who is (two names, then "and N others"); `ChatAvailabilitySettings` for your own.
|
|
219
267
|
- **Broadcast**: `<ChatBroadcast resolveUser={…} kind="direct" />` picks conversations from the inbox
|
|
@@ -221,7 +269,9 @@ generic card. Errors go to the console.
|
|
|
221
269
|
- **Search**: `<ChatSearch resolveUser onOpen={(hit) => …} conversationId? />`; the thread header has
|
|
222
270
|
it for one conversation and jumps to the message (loading earlier pages); `ChatPanel` has it
|
|
223
271
|
across all (`search={false}` hides it) and opens the conversation at the message
|
|
224
|
-
(`ChatThread jumpTo={{ messageId, seq }}`).
|
|
272
|
+
(`ChatThread jumpTo={{ messageId, seq, threadRootId }}`). A hit that is a reply in a thread
|
|
273
|
+
calls `onOpenThread(root, { jumpTo: { messageId, seq } })` (pass `jumpTo` to the thread's own
|
|
274
|
+
`ChatThread`) and scrolls the timeline to the thread's first message.
|
|
225
275
|
- **Link previews**: `ChatLinkPreview` under a message with a link: plain text only, no referrer,
|
|
226
276
|
and **no image by default**. An og:image lives on the linked site, so loading it in readers'
|
|
227
277
|
browsers tells that site (which the sender may control) each reader's IP and when they read it.
|
|
@@ -420,6 +470,31 @@ the file to `url` and sends the attachment with `provider_id: providerId`. The c
|
|
|
420
470
|
nothing the webhook trusts: the server sets the upload's passthrough to the conversation and
|
|
421
471
|
uploader. A refusal answers 403; over the cap, 429.
|
|
422
472
|
|
|
473
|
+
### A fake video provider for development
|
|
474
|
+
|
|
475
|
+
`fakeVideoProvider` stands in for Mux locally, so the hosted path (pending, upload, ready) runs
|
|
476
|
+
without an account. It throws when `NODE_ENV` is `production` unless you pass `force: true`.
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { createVideoUploadHandler, fakeVideoProvider } from "@marktiderman/genesis-chat/server";
|
|
480
|
+
const video = fakeVideoProvider({
|
|
481
|
+
supabase: serviceRoleClient, // writes the bucket, calls update_attachment
|
|
482
|
+
uploadUrl: "http://localhost:3000/api/dev/fake-video", // where you mount video.handler
|
|
483
|
+
});
|
|
484
|
+
export const mint = createVideoUploadHandler({ video, authorize, corsOrigin: "http://localhost:3000" });
|
|
485
|
+
export const PUT = video.handler; // takes the bytes, stores them, marks the attachment ready
|
|
486
|
+
export const OPTIONS = video.handler;
|
|
487
|
+
// Or by hand, as Mux's video.asset.ready webhook would:
|
|
488
|
+
await video.markReady(providerId, { durationMs: 4200, posterPath });
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
`createUpload` returns `uploadUrl?id=fake_<uuid>`. `handler` stores the PUT body in the genesis-chat
|
|
492
|
+
bucket under the upload's conversation and uploader, then calls `markReady`, waiting up to
|
|
493
|
+
`readyWaitMs` (10 s) for the message carrying the upload; past that it answers 202 and you can call
|
|
494
|
+
`markReady` later. `markReady` calls `genesis_chat.update_attachment` with status ready, playback id
|
|
495
|
+
`fake-<upload id>`, a fake HLS URL (`https://fake-video.invalid/...`) and the duration and poster
|
|
496
|
+
you give. `video.playbackUrl(playbackId)` signs a link to the stored file.
|
|
497
|
+
|
|
423
498
|
### AI recaps and reply drafts
|
|
424
499
|
|
|
425
500
|
```ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomId, attachmentKindFor, ChatError } from './chunk-
|
|
1
|
+
import { randomId, attachmentKindFor, ChatError } from './chunk-GBJ3YI4E.js';
|
|
2
2
|
|
|
3
3
|
// src/core/thread.ts
|
|
4
4
|
function mergeThread(server, pending) {
|
|
@@ -122,7 +122,13 @@ function previewText(body, attachment, deleted = false) {
|
|
|
122
122
|
|
|
123
123
|
// src/core/outbox.ts
|
|
124
124
|
function describe(f) {
|
|
125
|
-
return {
|
|
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
|
+
};
|
|
126
132
|
}
|
|
127
133
|
var memoryOutboxStore = () => {
|
|
128
134
|
let saved = [];
|
|
@@ -142,6 +148,8 @@ function createOutbox(opts) {
|
|
|
142
148
|
const running = /* @__PURE__ */ new Map();
|
|
143
149
|
const aborts = /* @__PURE__ */ new Map();
|
|
144
150
|
const uploaded = /* @__PURE__ */ new Map();
|
|
151
|
+
const transfers = /* @__PURE__ */ new Map();
|
|
152
|
+
const hostedAborts = /* @__PURE__ */ new Map();
|
|
145
153
|
const release = opts.releasePreview ?? releaseObjectUrl;
|
|
146
154
|
const releasePreviews = (q) => {
|
|
147
155
|
for (const url of /* @__PURE__ */ new Set([...Object.values(q.previews ?? {}), ...q.files.map((f) => f.previewUrl)])) {
|
|
@@ -168,6 +176,19 @@ function createOutbox(opts) {
|
|
|
168
176
|
opts.onChange?.([...queue]);
|
|
169
177
|
void store.save(queue.map(saveable));
|
|
170
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
|
+
};
|
|
171
192
|
async function persist(q) {
|
|
172
193
|
if (!opts.persistFile) return;
|
|
173
194
|
for (let i = 0; i < q.files.length; i++) {
|
|
@@ -193,8 +214,10 @@ function createOutbox(opts) {
|
|
|
193
214
|
const live = new Set(queue.map((q) => q.clientId));
|
|
194
215
|
const restored = saved.filter((q) => !live.has(q.clientId)).map((q) => {
|
|
195
216
|
const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;
|
|
217
|
+
const rest = { ...q };
|
|
218
|
+
delete rest.sentMessageId;
|
|
196
219
|
return {
|
|
197
|
-
...
|
|
220
|
+
...rest,
|
|
198
221
|
status: q.status === "sending" || lost > 0 ? "failed" : q.status,
|
|
199
222
|
error: lost > 0 ? "Attach the file again to send" : q.error ?? "Not sent",
|
|
200
223
|
files: q.files ?? [],
|
|
@@ -209,10 +232,62 @@ function createOutbox(opts) {
|
|
|
209
232
|
})();
|
|
210
233
|
return loading;
|
|
211
234
|
}
|
|
212
|
-
async function uploadFile(conversationId, file, signal) {
|
|
213
|
-
if (opts.upload) return opts.upload(conversationId, file, { signal });
|
|
235
|
+
async function uploadFile(conversationId, file, signal, onProgress) {
|
|
236
|
+
if (opts.upload) return opts.upload(conversationId, file, { signal, onProgress });
|
|
214
237
|
const data = typeof file.data === "string" ? await (opts.readFile ?? notReadable)(file.data) : file.data;
|
|
215
|
-
return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType });
|
|
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);
|
|
216
291
|
}
|
|
217
292
|
async function run(conversationId) {
|
|
218
293
|
const gen = generation;
|
|
@@ -222,7 +297,7 @@ function createOutbox(opts) {
|
|
|
222
297
|
try {
|
|
223
298
|
for (; ; ) {
|
|
224
299
|
if (gen !== generation) return;
|
|
225
|
-
const next = queue.find((q) => q.input.conversationId === conversationId);
|
|
300
|
+
const next = queue.find((q) => q.input.conversationId === conversationId && !q.sentMessageId);
|
|
226
301
|
if (!next || next.status !== "sending") break;
|
|
227
302
|
if ((next.lostFiles ?? 0) > 0) {
|
|
228
303
|
next.status = "failed";
|
|
@@ -236,10 +311,25 @@ function createOutbox(opts) {
|
|
|
236
311
|
const attachments = [...next.input.attachments ?? []];
|
|
237
312
|
for (let i = 0; i < next.files.length; i++) {
|
|
238
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
|
+
}
|
|
239
328
|
const key = `${next.clientId}:${i}`;
|
|
240
329
|
let path = uploaded.get(key);
|
|
241
330
|
if (!path) {
|
|
242
|
-
|
|
331
|
+
const index = i;
|
|
332
|
+
path = await uploadFile(conversationId, file, controller?.signal, (sent, total) => setProgress(next, index, sent, total));
|
|
243
333
|
if (stale(next)) return;
|
|
244
334
|
uploaded.set(key, path);
|
|
245
335
|
}
|
|
@@ -254,8 +344,16 @@ function createOutbox(opts) {
|
|
|
254
344
|
if (stale(next)) return;
|
|
255
345
|
const message = await opts.client.send({ ...next.input, attachments });
|
|
256
346
|
if (gen !== generation) return;
|
|
257
|
-
queue = queue.filter((q) => q.clientId !== next.clientId);
|
|
258
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);
|
|
259
357
|
emit();
|
|
260
358
|
opts.onSent?.(message, next.clientId);
|
|
261
359
|
releasePreviews(next);
|
|
@@ -276,7 +374,11 @@ function createOutbox(opts) {
|
|
|
276
374
|
if (running.get(conversationId) === gen) running.delete(conversationId);
|
|
277
375
|
}
|
|
278
376
|
}
|
|
279
|
-
const canAutoRetry = (x) => !x.permanent && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== "string" || Boolean(opts.readFile || opts.upload));
|
|
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
|
+
};
|
|
280
382
|
return {
|
|
281
383
|
/** Queues a message; it shows at once and sends in order. Returns its clientId. */
|
|
282
384
|
async send(input, files = []) {
|
|
@@ -313,7 +415,7 @@ function createOutbox(opts) {
|
|
|
313
415
|
for (const x of queue) {
|
|
314
416
|
if (x.input.conversationId !== q.input.conversationId || x.status !== "failed") continue;
|
|
315
417
|
if ((x.lostFiles ?? 0) > 0) break;
|
|
316
|
-
x
|
|
418
|
+
requeue(x);
|
|
317
419
|
x.permanent = false;
|
|
318
420
|
}
|
|
319
421
|
emit();
|
|
@@ -364,7 +466,7 @@ function createOutbox(opts) {
|
|
|
364
466
|
const gen = generation;
|
|
365
467
|
await persist(q);
|
|
366
468
|
if (gen !== generation || !queue.includes(q)) return;
|
|
367
|
-
q
|
|
469
|
+
requeue(q);
|
|
368
470
|
emit();
|
|
369
471
|
void run(q.input.conversationId);
|
|
370
472
|
},
|
|
@@ -381,10 +483,10 @@ function createOutbox(opts) {
|
|
|
381
483
|
const c = x.input.conversationId;
|
|
382
484
|
if (held.has(c) || x.status !== "failed") continue;
|
|
383
485
|
if (!canAutoRetry(x)) {
|
|
384
|
-
held.add(c);
|
|
486
|
+
if (!x.sentMessageId) held.add(c);
|
|
385
487
|
continue;
|
|
386
488
|
}
|
|
387
|
-
x
|
|
489
|
+
requeue(x);
|
|
388
490
|
conversations.add(c);
|
|
389
491
|
}
|
|
390
492
|
emit();
|
|
@@ -395,6 +497,9 @@ function createOutbox(opts) {
|
|
|
395
497
|
generation += 1;
|
|
396
498
|
for (const c of aborts.values()) c.abort();
|
|
397
499
|
aborts.clear();
|
|
500
|
+
for (const c of hostedAborts.values()) c.abort();
|
|
501
|
+
hostedAborts.clear();
|
|
502
|
+
transfers.clear();
|
|
398
503
|
running.clear();
|
|
399
504
|
const gone = queue;
|
|
400
505
|
queue = [];
|
|
@@ -410,9 +515,18 @@ function createOutbox(opts) {
|
|
|
410
515
|
queue = queue.filter((q) => q.clientId !== clientId);
|
|
411
516
|
aborts.get(clientId)?.abort();
|
|
412
517
|
aborts.delete(clientId);
|
|
518
|
+
stopHosted(clientId);
|
|
413
519
|
if (gone) releasePreviews(gone);
|
|
414
520
|
emit();
|
|
415
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
|
+
},
|
|
416
530
|
pending(conversationId) {
|
|
417
531
|
return queue.filter((q) => !conversationId || q.input.conversationId === conversationId).map((q) => ({ ...q, files: q.pendingFiles }));
|
|
418
532
|
},
|
|
@@ -426,5 +540,5 @@ async function notReadable() {
|
|
|
426
540
|
}
|
|
427
541
|
|
|
428
542
|
export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, releaseObjectUrl, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages };
|
|
429
|
-
//# sourceMappingURL=chunk-
|
|
430
|
-
//# sourceMappingURL=chunk-
|
|
543
|
+
//# sourceMappingURL=chunk-A4YJX7KT.js.map
|
|
544
|
+
//# sourceMappingURL=chunk-A4YJX7KT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/thread.ts","../src/core/outbox.ts"],"names":[],"mappings":"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","file":"chunk-A4YJX7KT.js","sourcesContent":["/**\n * @feat:chat\n * Pure thread logic, the same on every platform: merging the server's messages\n * with ones still on their way, grouping a run of messages from one sender,\n * day dividers, the unread line, reactions by emoji, and the typing line.\n */\nimport type { ChatActivity, ChatMessage, PendingMessage } from \"./types\";\n\nexport type ThreadItem =\n | { type: \"message\"; message: ChatMessage; pending?: undefined }\n | { type: \"pending\"; pending: PendingMessage; message?: undefined };\n\n/**\n * The server's messages (by seq), then what is still sending, oldest first. A\n * pending message the server already has (same clientId) is dropped, so a send\n * that landed never shows twice.\n */\nexport function mergeThread(server: ChatMessage[], pending: PendingMessage[]): ThreadItem[] {\n const byId = new Map<string, ChatMessage>();\n for (const m of server) byId.set(m.id, m);\n const sorted = [...byId.values()].sort((a, b) => a.seq - b.seq);\n const landed = new Set(sorted.map((m) => m.clientId));\n const waiting = pending\n .filter((p) => !landed.has(p.clientId))\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n return [\n ...sorted.map((message) => ({ type: \"message\" as const, message })),\n ...waiting.map((p) => ({ type: \"pending\" as const, pending: p })),\n ];\n}\n\n/** Adds or replaces messages in a page, keeping it sorted and free of duplicates. */\nexport function upsertMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {\n const byId = new Map(existing.map((m) => [m.id, m]));\n for (const m of incoming) byId.set(m.id, m);\n return [...byId.values()].sort((a, b) => a.seq - b.seq);\n}\n\nexport function itemSender(item: ThreadItem): string | null {\n return item.type === \"message\" ? item.message.senderId : item.pending.senderId;\n}\n\nexport function itemTime(item: ThreadItem): string {\n return item.type === \"message\" ? item.message.createdAt : item.pending.createdAt;\n}\n\n/** Runs of one sender within five minutes share a header (name, avatar). */\nexport function startsGroup(items: ThreadItem[], index: number, gapMs = 5 * 60_000): boolean {\n if (index === 0) return true;\n const prev = items[index - 1]!;\n const cur = items[index]!;\n if (cur.type === \"message\" && cur.message.kind === \"system\") return true;\n if (prev.type === \"message\" && prev.message.kind === \"system\") return true;\n if (itemSender(prev) !== itemSender(cur)) return true;\n return Date.parse(itemTime(cur)) - Date.parse(itemTime(prev)) > gapMs || !sameDay(itemTime(prev), itemTime(cur));\n}\n\nexport function sameDay(a: string, b: string): boolean {\n const x = new Date(a);\n const y = new Date(b);\n return x.getFullYear() === y.getFullYear() && x.getMonth() === y.getMonth() && x.getDate() === y.getDate();\n}\n\n/** A day divider goes before this item. */\nexport function startsDay(items: ThreadItem[], index: number): boolean {\n return index === 0 || !sameDay(itemTime(items[index - 1]!), itemTime(items[index]!));\n}\n\n/** \"Today\", \"Yesterday\", \"Monday\", or \"12 Mar 2026\". */\nexport function dayLabel(iso: string, now = new Date(), locale?: string): string {\n const d = new Date(iso);\n const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(now) - startOf(d)) / 86_400_000);\n if (days === 0) return \"Today\";\n if (days === 1) return \"Yesterday\";\n if (days > 1 && days < 7) return d.toLocaleDateString(locale, { weekday: \"long\" });\n return d.toLocaleDateString(locale, { day: \"numeric\", month: \"short\", year: d.getFullYear() === now.getFullYear() ? undefined : \"numeric\" });\n}\n\n/** The first message after what you had read, from someone else: where \"New\" goes. */\nexport function firstUnreadId(messages: ChatMessage[], lastReadSeq: number, me: string): string | null {\n return messages.find((m) => m.seq > lastReadSeq && m.senderId !== me && !m.deletedAt)?.id ?? null;\n}\n\nexport interface ReactionSummary {\n emoji: string;\n count: number;\n mine: boolean;\n userIds: string[];\n}\n\n/** Reactions by emoji, most first, then in first-used order. */\nexport function summarizeReactions(message: ChatMessage, me: string): ReactionSummary[] {\n const order: string[] = [];\n const by = new Map<string, string[]>();\n for (const r of message.reactions) {\n if (!by.has(r.emoji)) {\n by.set(r.emoji, []);\n order.push(r.emoji);\n }\n by.get(r.emoji)!.push(r.userId);\n }\n return order\n .map((emoji) => ({ emoji, count: by.get(emoji)!.length, mine: by.get(emoji)!.includes(me), userIds: by.get(emoji)! }))\n .sort((a, b) => b.count - a.count || order.indexOf(a.emoji) - order.indexOf(b.emoji));\n}\n\n/** Who is doing what now: activity older than six seconds has stopped. */\nexport function currentActivity(activity: ChatActivity[], now = Date.now(), staleMs = 6000): ChatActivity[] {\n const latest = new Map<string, ChatActivity>();\n for (const a of activity) {\n if (now - a.at > staleMs) continue;\n const prev = latest.get(a.userId);\n if (!prev || prev.at < a.at) latest.set(a.userId, a);\n }\n return [...latest.values()];\n}\n\n/** \"Sam is typing…\", \"Sam and Lee are typing…\", \"Sam is recording a voice message…\". */\nexport function activityLine(activity: ChatActivity[], nameOf: (userId: string) => string): string | null {\n if (activity.length === 0) return null;\n const recording = activity.find((a) => a.activity !== \"typing\");\n if (recording) {\n return `${nameOf(recording.userId)} is recording ${recording.activity === \"recording-video\" ? \"a video\" : \"a voice message\"}…`;\n }\n const names = activity.map((a) => nameOf(a.userId));\n if (names.length === 1) return `${names[0]} is typing…`;\n if (names.length === 2) return `${names[0]} and ${names[1]} are typing…`;\n return \"Several people are typing…\";\n}\n\n/** Sends activity at most once every `everyMs`. */\nexport function throttle(fn: () => void, everyMs = 3000): () => void {\n let last = 0;\n return () => {\n const now = Date.now();\n if (now - last >= everyMs) {\n last = now;\n fn();\n }\n };\n}\n\n/** The ids @mentioned in markdown written as `@[Name](user:<uuid>)`. */\nexport function parseMentions(body: string): string[] {\n const ids = new Set<string>();\n for (const m of body.matchAll(/@\\[[^\\]]{1,80}\\]\\(user:([0-9a-f-]{36})\\)/gi)) ids.add(m[1]!.toLowerCase());\n return [...ids];\n}\n\n/** A one-line preview: text, or what was attached. */\nexport function previewText(body: string, attachment: string | null | undefined, deleted = false): string {\n if (deleted) return \"Message deleted\";\n const text = body.replace(/@\\[([^\\]]+)\\]\\(user:[0-9a-f-]{36}\\)/gi, \"@$1\").replace(/[*_`#>~]/g, \"\").trim();\n if (text) return text.length > 140 ? `${text.slice(0, 139)}…` : text;\n switch (attachment) {\n case \"image\":\n return \"Photo\";\n case \"voice\":\n return \"Voice message\";\n case \"video\":\n return \"Video\";\n case \"file\":\n return \"File\";\n case null:\n case undefined:\n return \"\";\n default:\n return \"Attachment\";\n }\n}\n","/**\n * @feat:chat\n * The send queue: a message shows at once as pending, its files upload, then it\n * is sent with the clientId made when it was written, so a retry (after a lost\n * connection, a closed app) never posts twice. Messages to one conversation go\n * in the order they were written: the first that can't go (a lost file, a refusal)\n * holds back the ones after it. A refusal (not allowed, bad data) is `permanent`\n * and only sent again by hand. `clear()` (sign-out) stops sends in flight: none\n * posts, or saves anything, afterwards. Storage is the platform's (memory by default;\n * localStorage on web, MMKV or SQLite on native), behind OutboxStore.\n *\n * A hosted video (`LocalFile.hosted`, with `hostedUpload`) doesn't wait for its bytes: the\n * upload is started (the provider's upload id minted), the message is sent in its place in the\n * order with the attachment `pending`, and the bytes carry on going up; the provider's webhook\n * marks it ready. The message stays here (hidden from the thread once the server has it) until\n * its bytes are up, so a relaunch carries the upload on (`resume`) and sends the message again\n * with the same clientId, which posts nothing twice.\n */\nimport type { ChatClient } from \"./client\";\nimport { ChatError } from \"./client\";\nimport { attachmentKindFor, randomId } from \"./media\";\nimport type { UploadProgress } from \"./transfer\";\nimport type { AttachmentInput, ChatMessage, HostedAttachment, PendingFile, PendingMessage, SendInput } from \"./types\";\n\n/** A file still on the device, uploaded when its message is sent. */\nexport interface LocalFile {\n /** The bytes, or (native) a file URI the platform reads in `readFile`. */\n data: Blob | ArrayBuffer | string;\n name: string;\n contentType: string;\n /** A preview to show while it uploads (an object URL, a file URI). The outbox owns it from here: see `releasePreview`. */\n previewUrl?: string;\n /** Known up front: a voice note's duration and waveform, a photo's size. */\n meta?: Partial<Omit<AttachmentInput, \"path\" | \"kind\">> & { kind?: AttachmentInput[\"kind\"] };\n /**\n * Send it to the hosted video provider (the outbox's `hostedUpload`) instead of storage: the\n * message goes at once, in order, with the attachment pending, while the bytes go up. Without\n * a `hostedUpload` it goes to storage like any file.\n */\n hosted?: boolean;\n}\n\n/** A hosted upload as the outbox keeps it (saved with the message), so it can be carried on. */\nexport interface HostedUploadRecord {\n provider: HostedAttachment[\"provider\"];\n providerId: string;\n /** Where the bytes go, when the hook returned it: what a resume needs. */\n url?: string;\n /** Every byte is there. */\n uploaded?: boolean;\n}\n\n/** What `hostedUpload` resolves with as soon as the upload exists (before the bytes are up). */\nexport interface HostedUploadStart extends HostedAttachment {\n /** Where the bytes go; kept with the message, handed back as `resume.url`. */\n url?: string;\n /** Settles when every byte is there (rejects when the transfer fails). Absent: already there. */\n done?: Promise<void>;\n}\n\nexport interface HostedUploadOptions {\n /** Aborts when the message is discarded or the outbox cleared. */\n signal?: AbortSignal;\n onProgress?: UploadProgress;\n /**\n * The upload started before (a relaunch, a retry after the transfer failed): carry it on and\n * resolve with the same `providerId`. A message may already be posted with it, so mint a new\n * one only when the old upload is gone; that message's video then stays pending.\n */\n resume?: HostedUploadRecord;\n}\n\n/** Starts (or carries on) a hosted upload; resolves once the provider's upload id exists. */\nexport type HostedUpload = (conversationId: string, file: LocalFile, opts: HostedUploadOptions) => Promise<HostedUploadStart>;\n\n/** Uploads a file to storage and resolves with its path. */\nexport type StorageUpload = (conversationId: string, file: LocalFile, opts?: { signal?: AbortSignal; onProgress?: UploadProgress }) => Promise<string>;\n\nexport interface QueuedMessage extends Omit<PendingMessage, \"files\"> {\n files: LocalFile[];\n /** What each file is, kept with the saved copy (PendingMessage.files). */\n pendingFiles?: PendingFile[];\n /** Files lost on a relaunch (bytes that weren't persisted). */\n lostFiles?: number;\n attempts: number;\n /** Hosted uploads started, by file slot. */\n hosted?: Record<number, HostedUploadRecord>;\n /** Posted (this message's id): it waits here only for its hosted bytes to finish. */\n sentMessageId?: string;\n}\n\nfunction describe(f: LocalFile): PendingFile {\n return {\n name: f.name,\n contentType: f.contentType,\n kind: f.meta?.kind ?? attachmentKindFor(f.contentType),\n meta: f.meta,\n ...(f.hosted ? { hosted: true } : {}),\n };\n}\n\nexport interface OutboxStore {\n load(): Promise<QueuedMessage[]>;\n save(queue: QueuedMessage[]): Promise<void>;\n}\n\nexport const memoryOutboxStore = (): OutboxStore => {\n let saved: QueuedMessage[] = [];\n return { load: async () => saved, save: async (q) => void (saved = q) };\n};\n\nexport interface OutboxOptions {\n client: ChatClient;\n userId: string;\n store?: OutboxStore;\n /** Reads a native file URI into bytes for upload. */\n readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;\n /**\n * Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts\n * when the message is discarded or the outbox cleared; call `onProgress(sent, total)` as bytes\n * go, and the pending bubble shows it (`PendingMessage.files[i].progress`).\n */\n upload?: StorageUpload;\n /**\n * Starts a hosted video upload for a file marked `hosted` and resolves once the provider's\n * upload id exists, with `done` for the bytes. See HostedUpload; without it, hosted files go\n * to storage.\n */\n hostedUpload?: HostedUpload;\n /** Called with every change, for the UI. */\n onChange?: (queue: QueuedMessage[]) => void;\n /** Called when a message lands. */\n onSent?: (message: ChatMessage, clientId: string) => void;\n /** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */\n newId?: () => string;\n /**\n * Keeps a file held as bytes (a pasted image, a recording) between launches: write it\n * somewhere and return a URI `readFile` can read (native: the app's cache directory).\n * Without it, such a file isn't saved, and after a relaunch its message asks for it again.\n */\n persistFile?: (file: LocalFile) => Promise<string>;\n /**\n * Lets go of a file's `previewUrl` once its message has landed, been discarded or cleared.\n * Default: revokes `blob:` object URLs (the web's), and leaves everything else alone.\n */\n releasePreview?: (url: string) => void;\n}\n\n/** Revokes a `blob:` object URL; anything else (a native file URI) is left alone. */\nexport function releaseObjectUrl(url: string) {\n if (!url.startsWith(\"blob:\") || typeof URL === \"undefined\" || typeof URL.revokeObjectURL !== \"function\") return;\n try {\n URL.revokeObjectURL(url);\n } catch {\n // Already gone, or a runtime without object URLs.\n }\n}\n\n\nexport function createOutbox(opts: OutboxOptions) {\n const store = opts.store ?? memoryOutboxStore();\n let queue: QueuedMessage[] = [];\n // Bumped by clear(): work begun under an older generation stops at its next await.\n let generation = 0;\n const running = new Map<string, number>(); // conversationId -> the generation running it\n const aborts = new Map<string, AbortController>(); // clientId -> its uploads' controller\n const uploaded = new Map<string, string>(); // clientId:index -> path\n // Hosted bytes still going up, by clientId:index, and the controller that stops them.\n const transfers = new Map<string, Promise<void>>();\n const hostedAborts = new Map<string, AbortController>();\n\n const release = opts.releasePreview ?? releaseObjectUrl;\n const releasePreviews = (q: QueuedMessage) => {\n for (const url of new Set([...Object.values(q.previews ?? {}), ...q.files.map((f) => f.previewUrl)])) {\n if (typeof url === \"string\" && url) release(url);\n }\n };\n\n const newId = opts.newId ?? randomId;\n\n // Saved copies: a file held as bytes is kept only as a URI (persistFile) or marked lost.\n // `files` holds the files still here, in slot order; `pendingFiles` holds every slot.\n const saveable = (q: QueuedMessage): QueuedMessage => {\n const slots = q.pendingFiles?.length ? q.pendingFiles : q.files.map(describe);\n const here = [...q.files];\n const files: LocalFile[] = [];\n const pendingFiles = slots.map((slot) => {\n if (slot.lost) return slot;\n const f = here.shift();\n if (f && typeof f.data === \"string\") {\n files.push(f);\n return { ...slot, lost: false };\n }\n return { ...slot, lost: true };\n });\n return { ...q, files, pendingFiles };\n };\n\n const emit = () => {\n opts.onChange?.([...queue]);\n void store.save(queue.map(saveable));\n };\n\n /** A file's upload progress, for the UI only (not saved). */\n const setProgress = (q: QueuedMessage, index: number, sent: number, total: number) => {\n const slots = q.pendingFiles;\n if (!slots?.[index] || !queue.includes(q)) return;\n // Whole percents: the bar shows no finer, and each change re-renders every chat hook.\n const progress = total > 0 ? Math.round(Math.min(1, Math.max(0, sent / total)) * 100) / 100 : 0;\n if (slots[index]!.progress === progress) return;\n q.pendingFiles = slots.map((slot, j) => (j === index ? { ...slot, progress } : slot));\n opts.onChange?.([...queue]);\n };\n\n const stopHosted = (clientId: string) => {\n hostedAborts.get(clientId)?.abort();\n hostedAborts.delete(clientId);\n for (const k of [...transfers.keys()]) if (k.startsWith(`${clientId}:`)) transfers.delete(k);\n };\n\n /** Swaps a byte file for a saved URI, so it survives a relaunch. */\n async function persist(q: QueuedMessage) {\n if (!opts.persistFile) return;\n for (let i = 0; i < q.files.length; i++) {\n const f = q.files[i]!;\n if (typeof f.data === \"string\") continue;\n try {\n q.files[i] = { ...f, data: await opts.persistFile(f) };\n } catch {\n // Kept in memory only; a relaunch asks for it again.\n }\n }\n }\n\n /**\n * One load for the outbox's life, which every method awaits. What was saved is merged into\n * the live queue (a send made while the load was in flight stays), deduped by clientId.\n */\n let loading: Promise<void> | null = null;\n function ensureLoaded(): Promise<void> {\n loading ??= (async () => {\n const gen = generation;\n let saved: QueuedMessage[] = [];\n try {\n saved = (await store.load()) ?? [];\n } catch {\n saved = [];\n }\n // Cleared (signed out) while it loaded: what was saved belonged to that session.\n if (gen !== generation) return;\n // A file held as bytes that wasn't persisted doesn't survive a relaunch: its message waits,\n // failed, for the file to be attached again (never sent without it) or discarded.\n const live = new Set(queue.map((q) => q.clientId));\n const restored: QueuedMessage[] = saved\n .filter((q) => !live.has(q.clientId))\n .map((q) => {\n const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;\n // Posted, its hosted bytes still going up when the app closed: it is sent again (the\n // same clientId posts nothing twice) after its upload is carried on.\n const rest: QueuedMessage = { ...q };\n delete rest.sentMessageId;\n return {\n ...rest,\n status: q.status === \"sending\" || lost > 0 ? \"failed\" : q.status,\n error: lost > 0 ? \"Attach the file again to send\" : (q.error ?? \"Not sent\"),\n files: q.files ?? [],\n attempts: q.attempts ?? 0,\n lostFiles: lost,\n };\n });\n if (restored.length) {\n queue = [...restored, ...queue];\n opts.onChange?.([...queue]);\n }\n })();\n return loading;\n }\n\n async function uploadFile(conversationId: string, file: LocalFile, signal?: AbortSignal, onProgress?: UploadProgress): Promise<string> {\n if (opts.upload) return opts.upload(conversationId, file, { signal, onProgress });\n const data = typeof file.data === \"string\" ? await (opts.readFile ?? notReadable)(file.data) : file.data;\n return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType, onProgress, signal });\n }\n\n const isHosted = (f: LocalFile) => Boolean(f.hosted && opts.hostedUpload);\n\n /**\n * The hosted upload for one file slot: the one going up, or a new (or carried-on) one. Resolves\n * once the provider's upload id exists; its bytes go on in `transfers`.\n */\n async function startHosted(q: QueuedMessage, index: number, file: LocalFile): Promise<HostedUploadRecord> {\n const key = `${q.clientId}:${index}`;\n const known = q.hosted?.[index];\n if (known && (known.uploaded || transfers.has(key))) return known;\n let controller = hostedAborts.get(q.clientId);\n if (!controller && typeof AbortController !== \"undefined\") {\n controller = new AbortController();\n hostedAborts.set(q.clientId, controller);\n }\n const started = await opts.hostedUpload!(q.input.conversationId, file, {\n signal: controller?.signal,\n onProgress: (sent, total) => setProgress(q, index, sent, total),\n ...(known ? { resume: known } : {}),\n });\n const record: HostedUploadRecord = { provider: started.provider, providerId: started.providerId, ...(started.url ? { url: started.url } : {}) };\n q.hosted = { ...q.hosted, [index]: record };\n // Saved at once: a relaunch before the message goes carries this upload on, not a new one.\n if (queue.includes(q)) emit();\n const transfer = Promise.resolve(started.done).then(\n () => {\n record.uploaded = true;\n if (transfers.get(key) === transfer) transfers.delete(key);\n setProgress(q, index, 1, 1);\n },\n (e: unknown) => {\n if (transfers.get(key) === transfer) transfers.delete(key);\n throw e;\n },\n );\n transfer.catch(() => undefined);\n transfers.set(key, transfer);\n return record;\n }\n\n /** A posted message's hosted bytes: once they are all up, it leaves; a failed transfer marks it failed (Retry carries it on). */\n async function settleHosted(q: QueuedMessage, gen: number) {\n const waiting = Object.entries(q.hosted ?? {})\n .filter(([, r]) => !r.uploaded)\n .map(([i]) => transfers.get(`${q.clientId}:${i}`) ?? Promise.reject(new ChatError(\"The upload stopped\", undefined, \"upload\")));\n try {\n await Promise.all(waiting);\n } catch (e) {\n if (gen !== generation || !queue.includes(q) || q.status !== \"sending\") return;\n q.attempts += 1;\n q.status = \"failed\";\n q.error = e instanceof ChatError && e.code === \"GSABORT\" ? \"The upload was cancelled\" : \"The video didn't finish uploading\";\n emit();\n return;\n }\n if (gen !== generation || !queue.includes(q)) return;\n // A retry may have started more meanwhile: wait for those too.\n if (Object.values(q.hosted ?? {}).some((r) => !r.uploaded)) return void settleHosted(q, gen);\n queue = queue.filter((x) => x !== q);\n hostedAborts.delete(q.clientId);\n emit();\n releasePreviews(q);\n }\n\n async function run(conversationId: string) {\n const gen = generation;\n if (running.get(conversationId) === gen) return;\n running.set(conversationId, gen);\n // Cleared (signed out) or discarded while we waited: stop, sending and saving nothing.\n const stale = (q: QueuedMessage) => gen !== generation || !queue.includes(q);\n try {\n for (;;) {\n if (gen !== generation) return;\n // In the order written: the first message still here decides. One that failed (a lost\n // file, a refusal, no connection) holds back the ones after it until it is retried.\n // A posted message waiting only for its hosted bytes holds nothing back.\n const next = queue.find((q) => q.input.conversationId === conversationId && !q.sentMessageId);\n if (!next || next.status !== \"sending\") break;\n if ((next.lostFiles ?? 0) > 0) {\n next.status = \"failed\";\n next.error = \"Attach the file again to send\";\n emit();\n break;\n }\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n if (controller) aborts.set(next.clientId, controller);\n try {\n const attachments: AttachmentInput[] = [...(next.input.attachments ?? [])];\n for (let i = 0; i < next.files.length; i++) {\n const file = next.files[i]!;\n if (isHosted(file)) {\n const hosted = await startHosted(next, i, file);\n if (stale(next)) return;\n attachments.push({\n ...file.meta,\n kind: file.meta?.kind ?? attachmentKindFor(file.contentType),\n provider: hosted.provider,\n providerId: hosted.providerId,\n status: \"pending\",\n name: file.meta?.name ?? file.name,\n mime: file.contentType,\n });\n continue;\n }\n const key = `${next.clientId}:${i}`;\n let path = uploaded.get(key);\n if (!path) {\n const index = i;\n path = await uploadFile(conversationId, file, controller?.signal, (sent, total) => setProgress(next, index, sent, total));\n if (stale(next)) return;\n uploaded.set(key, path);\n }\n attachments.push({\n ...file.meta,\n kind: file.meta?.kind ?? attachmentKindFor(file.contentType),\n path,\n name: file.meta?.name ?? file.name,\n mime: file.contentType,\n });\n }\n if (stale(next)) return;\n const message = await opts.client.send({ ...next.input, attachments });\n // Posted, but the person signed out meanwhile: the next session must not hear of it.\n if (gen !== generation) return;\n for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);\n if (Object.values(next.hosted ?? {}).some((r) => !r.uploaded)) {\n // Posted with its video pending: it stays (out of the thread, which has the server's\n // copy) until the bytes are up, so a relaunch can carry the upload on.\n next.sentMessageId = message.id;\n emit();\n opts.onSent?.(message, next.clientId);\n void settleHosted(next, gen);\n continue;\n }\n queue = queue.filter((q) => q.clientId !== next.clientId);\n hostedAborts.delete(next.clientId);\n emit();\n opts.onSent?.(message, next.clientId);\n // The bubble now draws the stored copy: the local preview can go.\n releasePreviews(next);\n } catch (e) {\n if (stale(next)) return;\n const err = e instanceof ChatError ? e : new ChatError(e instanceof Error ? e.message : String(e), undefined, \"send\");\n next.attempts += 1;\n next.status = \"failed\";\n next.permanent = err.permanent;\n next.error = err.forbidden ? \"You can't post here\" : err.transient ? \"Not sent. Check your connection.\" : err.message;\n emit();\n // Later messages to this conversation wait behind a failed one, so order holds.\n break;\n } finally {\n if (controller && aborts.get(next.clientId) === controller) aborts.delete(next.clientId);\n }\n }\n } finally {\n if (running.get(conversationId) === gen) running.delete(conversationId);\n }\n }\n\n /** Can this failed message go without the person's help (retryAll, coming back online)? */\n const canAutoRetry = (x: QueuedMessage) =>\n !x.permanent &&\n !(x.lostFiles ?? 0) &&\n x.files.every((f) => typeof f.data !== \"string\" || Boolean(isHosted(f) ? opts.hostedUpload : opts.readFile || opts.upload));\n\n /** A failed message goes again: a posted one (its transfer failed) re-sends, idempotently, after carrying its upload on. */\n const requeue = (x: QueuedMessage) => {\n x.status = \"sending\";\n delete x.sentMessageId;\n };\n\n return {\n /** Queues a message; it shows at once and sends in order. Returns its clientId. */\n async send(input: Omit<SendInput, \"clientId\"> & { clientId?: string }, files: LocalFile[] = []): Promise<string> {\n await ensureLoaded();\n const gen = generation;\n const clientId = input.clientId ?? newId();\n const previews: Record<number, string> = {};\n files.forEach((f, i) => {\n if (f.previewUrl) previews[i] = f.previewUrl;\n });\n const queued: QueuedMessage = {\n clientId,\n input: { ...input, clientId },\n senderId: opts.userId,\n createdAt: new Date().toISOString(),\n status: \"sending\",\n previews,\n files: [...files],\n pendingFiles: files.map(describe),\n attempts: 0,\n };\n queue.push(queued);\n opts.onChange?.([...queue]);\n await persist(queued);\n // Cleared or discarded while its files were saved: it goes nowhere.\n if (gen !== generation || !queue.includes(queued)) return clientId;\n emit();\n void run(input.conversationId);\n return clientId;\n },\n\n async retry(clientId: string) {\n await ensureLoaded();\n const q = queue.find((x) => x.clientId === clientId);\n if (!q) return;\n // Retrying one retries everything failed in that conversation, in order, refusals too\n // (a person asked), up to one still waiting for a file.\n for (const x of queue) {\n if (x.input.conversationId !== q.input.conversationId || x.status !== \"failed\") continue;\n if ((x.lostFiles ?? 0) > 0) break;\n requeue(x);\n x.permanent = false;\n }\n emit();\n void run(q.input.conversationId);\n },\n\n /**\n * Gives a message the files it lost on a relaunch and sends it. `files` fill the lost\n * slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass\n * one per lost slot; with fewer, the message keeps waiting for the rest.\n */\n async reattach(clientId: string, files: LocalFile[]) {\n await ensureLoaded();\n const q = queue.find((x) => x.clientId === clientId);\n if (!q) return;\n const slots = q.pendingFiles ?? [];\n const kept = [...q.files];\n const given = [...files];\n const merged: LocalFile[] = [];\n const described: PendingFile[] = [];\n for (const slot of slots) {\n if (slot.lost) {\n const f = given.shift();\n if (f) {\n merged.push(f);\n described.push(describe(f));\n } else {\n described.push(slot);\n }\n } else {\n const f = kept.shift();\n if (f) {\n merged.push(f);\n described.push(slot);\n }\n }\n }\n // A message saved before slots were recorded: the given files are all of them.\n if (slots.length === 0) {\n merged.push(...given);\n described.push(...given.map(describe));\n }\n q.files = merged;\n q.pendingFiles = described;\n q.lostFiles = described.filter((d) => d.lost).length;\n if (q.lostFiles > 0) {\n emit();\n return;\n }\n const gen = generation;\n await persist(q);\n if (gen !== generation || !queue.includes(q)) return;\n requeue(q);\n emit();\n void run(q.input.conversationId);\n },\n\n /**\n * Retries failed messages (after the connection comes back), in order per conversation,\n * stopping at the first that can't go on its own: one waiting for a lost file, or one the\n * server refused (`permanent`, retried only by hand). The ones after it keep waiting.\n */\n async retryAll() {\n await ensureLoaded();\n const conversations = new Set<string>();\n const held = new Set<string>();\n for (const x of queue) {\n const c = x.input.conversationId;\n if (held.has(c) || x.status !== \"failed\") continue;\n if (!canAutoRetry(x)) {\n // A posted one (only its upload failed) holds nothing back.\n if (!x.sentMessageId) held.add(c);\n continue;\n }\n requeue(x);\n conversations.add(c);\n }\n emit();\n for (const c of conversations) void run(c);\n },\n\n /** Forgets every queued message, here and in the store (call it on sign-out). */\n async clear() {\n // At once, before any await: a send in flight stops at its next step and posts nothing.\n generation += 1;\n for (const c of aborts.values()) c.abort();\n aborts.clear();\n for (const c of hostedAborts.values()) c.abort();\n hostedAborts.clear();\n transfers.clear();\n running.clear();\n const gone = queue;\n queue = [];\n uploaded.clear();\n gone.forEach(releasePreviews);\n opts.onChange?.([]);\n await ensureLoaded();\n await store.save([]);\n },\n\n async discard(clientId: string) {\n await ensureLoaded();\n const gone = queue.find((q) => q.clientId === clientId);\n queue = queue.filter((q) => q.clientId !== clientId);\n aborts.get(clientId)?.abort();\n aborts.delete(clientId);\n stopHosted(clientId);\n if (gone) releasePreviews(gone);\n emit();\n },\n\n /**\n * Uploads one file to storage the way the outbox does (the `upload` hook, or `readFile` then\n * the client's upload) and resolves with its path: for files sent outside the queue, such as\n * a scheduled message's.\n */\n uploadFile(conversationId: string, file: LocalFile, o: { signal?: AbortSignal; onProgress?: UploadProgress } = {}): Promise<string> {\n return uploadFile(conversationId, file, o.signal, o.onProgress);\n },\n\n pending(conversationId?: string): PendingMessage[] {\n return queue\n .filter((q) => !conversationId || q.input.conversationId === conversationId)\n .map((q) => ({ ...q, files: q.pendingFiles }));\n },\n\n /** A new client id, made the way this outbox makes them. */\n newId,\n\n load: ensureLoaded,\n };\n}\n\nexport type Outbox = ReturnType<typeof createOutbox>;\n\nasync function notReadable(): Promise<never> {\n // This device's set-up (no readFile), not a refusal: a relaunch that can read it sends it.\n throw new ChatError(\"This file can't be read on this device\", \"GSFILE\", \"upload\");\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { previewText } from './chunk-A4YJX7KT.js';
|
|
2
|
+
|
|
3
|
+
// src/react/previews.ts
|
|
4
|
+
var callAttachmentKind = (kind, key, a, ctx) => kind?.[key]?.call(kind, a, ctx);
|
|
5
|
+
function cardTitle(kind, a, ctx = { mine: false }) {
|
|
6
|
+
try {
|
|
7
|
+
return callAttachmentKind(kind, "title", a, ctx) || a.name || a.kind;
|
|
8
|
+
} catch {
|
|
9
|
+
return a.name || a.kind;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function bareAttachment(kind) {
|
|
13
|
+
return { id: "", messageId: "", position: 0, kind, ref: {}, provider: "none", path: null, providerId: null, playbackId: null, url: null, status: "ready", name: null, mime: null, bytes: null, width: null, height: null, durationMs: null, waveform: null, posterPath: null, thumbhash: null, transcript: null, transcriptStatus: null };
|
|
14
|
+
}
|
|
15
|
+
function attachmentPreview(body, attachment, kinds, deleted = false) {
|
|
16
|
+
const kind = typeof attachment === "string" ? attachment : attachment?.kind;
|
|
17
|
+
const text = previewText(body, kind, deleted);
|
|
18
|
+
if (deleted || !kind || !kinds || !Object.prototype.hasOwnProperty.call(kinds, kind) || previewText(body, null)) return text;
|
|
19
|
+
const a = typeof attachment === "string" ? bareAttachment(attachment) : attachment;
|
|
20
|
+
const k = kinds[kind];
|
|
21
|
+
const title = typeof k?.label === "string" && k.label.trim() ? k.label.trim() : cardTitle(k, a);
|
|
22
|
+
return title && title !== kind ? title : text;
|
|
23
|
+
}
|
|
24
|
+
function messagePreview(message, kinds) {
|
|
25
|
+
return attachmentPreview(message.body, message.attachments[0], kinds, Boolean(message.deletedAt));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { attachmentPreview, callAttachmentKind, cardTitle, messagePreview };
|
|
29
|
+
//# sourceMappingURL=chunk-AZS3HNLM.js.map
|
|
30
|
+
//# sourceMappingURL=chunk-AZS3HNLM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/previews.ts"],"names":[],"mappings":";;;AAWO,IAAM,kBAAA,GAAqB,CAA4C,IAAA,EAAsC,GAAA,EAAQ,CAAA,EAAmB,GAAA,KAC5I,IAAA,GAAO,GAAG,CAAA,EAAoG,IAAA,CAAK,IAAA,EAAO,CAAA,EAAG,GAAG;AAK5H,SAAS,UAAU,IAAA,EAAsC,CAAA,EAAmB,MAAuB,EAAE,IAAA,EAAM,OAAM,EAAW;AACjI,EAAA,IAAI;AACF,IAAA,OAAO,kBAAA,CAAmB,MAAM,OAAA,EAAS,CAAA,EAAG,GAAG,CAAA,IAAK,CAAA,CAAE,QAAQ,CAAA,CAAE,IAAA;AAAA,EAClE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,CAAA,CAAE,QAAQ,CAAA,CAAE,IAAA;AAAA,EACrB;AACF;AAGA,SAAS,eAAe,IAAA,EAA8B;AACpD,EAAA,OAAO,EAAE,IAAI,EAAA,EAAI,SAAA,EAAW,IAAI,QAAA,EAAU,CAAA,EAAG,MAAM,GAAA,EAAK,IAAI,QAAA,EAAU,MAAA,EAAQ,MAAM,IAAA,EAAM,UAAA,EAAY,MAAM,UAAA,EAAY,IAAA,EAAM,KAAK,IAAA,EAAM,MAAA,EAAQ,SAAS,IAAA,EAAM,IAAA,EAAM,MAAM,IAAA,EAAM,KAAA,EAAO,MAAM,KAAA,EAAO,IAAA,EAAM,QAAQ,IAAA,EAAM,UAAA,EAAY,MAAM,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAM,SAAA,EAAW,MAAM,UAAA,EAAY,IAAA,EAAM,kBAAkB,IAAA,EAAK;AAC1U;AAOO,SAAS,iBAAA,CAAkB,IAAA,EAAc,UAAA,EAAwD,KAAA,EAAuD,UAAU,KAAA,EAAe;AACtL,EAAA,MAAM,IAAA,GAAO,OAAO,UAAA,KAAe,QAAA,GAAW,aAAa,UAAA,EAAY,IAAA;AACvE,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,OAAO,CAAA;AAE5C,EAAA,IAAI,WAAW,CAAC,IAAA,IAAQ,CAAC,KAAA,IAAS,CAAC,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,OAAO,IAAI,CAAA,IAAK,YAAY,IAAA,EAAM,IAAI,GAAG,OAAO,IAAA;AACxH,EAAA,MAAM,IAAI,OAAO,UAAA,KAAe,QAAA,GAAW,cAAA,CAAe,UAAU,CAAA,GAAI,UAAA;AACxE,EAAA,MAAM,CAAA,GAAI,MAAM,IAAI,CAAA;AACpB,EAAA,MAAM,KAAA,GAAQ,OAAO,CAAA,EAAG,KAAA,KAAU,YAAY,CAAA,CAAE,KAAA,CAAM,IAAA,EAAK,GAAI,EAAE,KAAA,CAAM,IAAA,EAAK,GAAI,SAAA,CAAU,GAAG,CAAC,CAAA;AAC9F,EAAA,OAAO,KAAA,IAAS,KAAA,KAAU,IAAA,GAAO,KAAA,GAAQ,IAAA;AAC3C;AAGO,SAAS,cAAA,CAAe,SAAsB,KAAA,EAA+D;AAClH,EAAA,OAAO,iBAAA,CAAkB,OAAA,CAAQ,IAAA,EAAM,OAAA,CAAQ,WAAA,CAAY,CAAC,CAAA,EAAG,KAAA,EAAO,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAC,CAAA;AAClG","file":"chunk-AZS3HNLM.js","sourcesContent":["/**\n * @feat:chat\n * One-line previews of a message (the inbox, the long-press sheet) where an app attachment kind\n * reads as its `label`, else its title (\"Check-in\"), not \"Attachment\". No React or DOM: web and\n * native both draw from it.\n */\nimport { previewText } from \"../core/thread\";\nimport type { ChatAttachment, ChatMessage } from \"../core/types\";\nimport type { ChatAttachmentKind, ChatCardContext } from \"./index\";\n\n/** Calls a kind's `title`, `subtitle` or `action` with the card context. */\nexport const callAttachmentKind = <K extends \"title\" | \"subtitle\" | \"action\">(kind: ChatAttachmentKind | undefined, key: K, a: ChatAttachment, ctx: ChatCardContext) =>\n (kind?.[key] as ((this: ChatAttachmentKind, a: ChatAttachment, ctx: ChatCardContext) => unknown) | undefined)?.call(kind!, a, ctx) as\n | ReturnType<NonNullable<ChatAttachmentKind[K]>>\n | undefined;\n\n/** A card's title (the kind's, else the file's name, else the kind); an app kind that throws reads as its name. */\nexport function cardTitle(kind: ChatAttachmentKind | undefined, a: ChatAttachment, ctx: ChatCardContext = { mine: false }): string {\n try {\n return callAttachmentKind(kind, \"title\", a, ctx) || a.name || a.kind;\n } catch {\n return a.name || a.kind;\n }\n}\n\n/** An attachment with only its kind, for a preview that knows no more (the inbox's last message). */\nfunction bareAttachment(kind: string): ChatAttachment {\n return { id: \"\", messageId: \"\", position: 0, kind, ref: {}, provider: \"none\", path: null, providerId: null, playbackId: null, url: null, status: \"ready\", name: null, mime: null, bytes: null, width: null, height: null, durationMs: null, waveform: null, posterPath: null, thumbhash: null, transcript: null, transcriptStatus: null };\n}\n\n/**\n * A message's one-line preview: its text, else what it carries, where an app kind reads as its\n * title (\"Check-in\") instead of \"Attachment\". `attachment` is the kind alone (the inbox) or the\n * attachment itself.\n */\nexport function attachmentPreview(body: string, attachment: string | ChatAttachment | null | undefined, kinds: Record<string, ChatAttachmentKind> | undefined, deleted = false): string {\n const kind = typeof attachment === \"string\" ? attachment : attachment?.kind;\n const text = previewText(body, kind, deleted);\n // Text that reads as something (not only markdown marks) is the preview, as previewText has it.\n if (deleted || !kind || !kinds || !Object.prototype.hasOwnProperty.call(kinds, kind) || previewText(body, null)) return text;\n const a = typeof attachment === \"string\" ? bareAttachment(attachment) : attachment!;\n const k = kinds[kind];\n const title = typeof k?.label === \"string\" && k.label.trim() ? k.label.trim() : cardTitle(k, a);\n return title && title !== kind ? title : text;\n}\n\n/** The long-press sheet's caption for a message. */\nexport function messagePreview(message: ChatMessage, kinds: Record<string, ChatAttachmentKind> | undefined): string {\n return attachmentPreview(message.body, message.attachments[0], kinds, Boolean(message.deletedAt));\n}\n\n"]}
|