@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.
- package/README.md +379 -13
- package/dist/chunk-A4YJX7KT.js +544 -0
- 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-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-GBJ3YI4E.js +966 -0
- package/dist/chunk-GBJ3YI4E.js.map +1 -0
- package/dist/chunk-JU2FAPBI.js +609 -0
- package/dist/chunk-JU2FAPBI.js.map +1 -0
- package/dist/chunk-VEPZROQB.js +187 -0
- package/dist/chunk-VEPZROQB.js.map +1 -0
- package/dist/{client-BtdALFCx.d.ts → client-DUIG1hum.d.ts} +187 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1939 -1
- package/dist/native.js +5023 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B9NAWQ58.d.ts +219 -0
- package/dist/push.d.ts +206 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +1 -0
- package/dist/react.d.ts +176 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +551 -21
- package/dist/server.js +976 -34
- package/dist/server.js.map +1 -1
- package/dist/thread-oPra7pkW.d.ts +57 -0
- package/dist/web.d.ts +335 -12
- package/dist/web.js +1784 -64
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/0003_genesis_chat_search_threads.sql +85 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +269 -0
- package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
- package/package.json +78 -6
- package/src/__tests__/coaching-core.test.ts +334 -0
- package/src/__tests__/coaching-react.test.tsx +319 -0
- package/src/__tests__/coaching-web.test.tsx +521 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/fake-xhr.ts +54 -0
- package/src/__tests__/outbox-hosted.test.ts +269 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-279.test.ts +41 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +405 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fake-video.test.ts +81 -0
- package/src/__tests__/server-fakes.ts +96 -0
- package/src/__tests__/server-integrations.test.ts +247 -0
- package/src/__tests__/server-providers.test.ts +246 -0
- package/src/__tests__/server-runner.test.ts +277 -0
- package/src/__tests__/server-safety.test.ts +303 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +349 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +783 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +635 -0
- package/src/core/thread.ts +171 -0
- package/src/core/transfer.ts +71 -0
- package/src/core/types.ts +328 -0
- package/src/index.ts +14 -0
- package/src/native/__tests__/auto-retry.test.tsx +128 -0
- package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
- package/src/native/__tests__/composer-state.test.ts +188 -0
- package/src/native/__tests__/composer.test.tsx +133 -0
- package/src/native/__tests__/emoji.test.tsx +254 -0
- package/src/native/__tests__/inbox-screen.test.tsx +48 -0
- package/src/native/__tests__/message-bubble.test.tsx +127 -0
- package/src/native/__tests__/organise-logic.test.ts +164 -0
- package/src/native/__tests__/organise-screens.test.tsx +424 -0
- package/src/native/__tests__/photo-viewer.test.tsx +226 -0
- package/src/native/__tests__/push-payload.test.ts +66 -0
- package/src/native/__tests__/push.test.ts +209 -0
- package/src/native/__tests__/recorder-machine.test.ts +96 -0
- package/src/native/__tests__/rows-actions.test.ts +76 -0
- package/src/native/__tests__/thread-layout.test.ts +41 -0
- package/src/native/__tests__/thread-screen.test.tsx +116 -0
- package/src/native/__tests__/tus.test.ts +154 -0
- package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
- package/src/native/__tests__/video-media.test.tsx +498 -0
- package/src/native/__tests__/video-recorder-state.test.ts +87 -0
- package/src/native/__tests__/video-upload.test.ts +227 -0
- package/src/native/access-error.tsx +31 -0
- package/src/native/actions.ts +42 -0
- package/src/native/attachment-cards.tsx +132 -0
- package/src/native/auto-retry.ts +69 -0
- package/src/native/availability.tsx +376 -0
- package/src/native/broadcast-sheet.tsx +193 -0
- package/src/native/composer-state.ts +302 -0
- package/src/native/composer.tsx +453 -0
- package/src/native/context.tsx +70 -0
- package/src/native/copy.ts +296 -0
- package/src/native/emoji-data.ts +21 -0
- package/src/native/emoji-picker.tsx +229 -0
- package/src/native/emoji.ts +118 -0
- package/src/native/icon-button.tsx +40 -0
- package/src/native/inbox-screen.tsx +122 -0
- package/src/native/index.ts +184 -0
- package/src/native/link-preview.tsx +104 -0
- package/src/native/media-colors.ts +15 -0
- package/src/native/media.tsx +187 -0
- package/src/native/message-actions-sheet.tsx +126 -0
- package/src/native/message-bubble.tsx +366 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +277 -0
- package/src/native/organise-copy.ts +195 -0
- package/src/native/photo-viewer.tsx +531 -0
- package/src/native/push.ts +375 -0
- package/src/native/recorder-machine.ts +176 -0
- package/src/native/rows.ts +81 -0
- package/src/native/schedule-sheet.tsx +328 -0
- package/src/native/search-screen.tsx +189 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +647 -0
- package/src/native/topics.tsx +234 -0
- package/src/native/tus.ts +282 -0
- package/src/native/use-composer.ts +71 -0
- package/src/native/use-jump.ts +91 -0
- package/src/native/use-voice-recorder.ts +234 -0
- package/src/native/video-player.tsx +410 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +274 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +301 -0
- package/src/react/index.tsx +894 -0
- package/src/react/previews.ts +51 -0
- package/src/server/assistant.ts +235 -0
- package/src/server/crypto.ts +86 -0
- package/src/server/expo.ts +119 -0
- package/src/server/fake-video.ts +171 -0
- package/src/server/handler.ts +32 -0
- package/src/server/index.ts +58 -0
- package/src/server/moderation.ts +155 -0
- package/src/server/mux.ts +290 -0
- package/src/server/notifications.ts +130 -0
- package/src/server/runner.ts +468 -0
- package/src/server/transcribers.ts +111 -0
- package/src/server/types.ts +171 -0
- package/src/server/unfurl.ts +480 -0
- package/src/server/upload.ts +158 -0
- package/src/web/attachments.tsx +379 -0
- package/src/web/availability.tsx +299 -0
- package/src/web/broadcast.tsx +166 -0
- package/src/web/bubble.tsx +585 -0
- package/src/web/cards.tsx +136 -0
- package/src/web/composer.tsx +571 -0
- package/src/web/emoji-picker.tsx +84 -0
- package/src/web/error-boundary.tsx +33 -0
- package/src/web/format.ts +55 -0
- package/src/web/inbox.tsx +196 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +714 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +204 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +830 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +516 -0
- package/src/web/voice-note.tsx +197 -0
- package/dist/chunk-27UTR5FW.js +0 -322
- package/dist/chunk-27UTR5FW.js.map +0 -1
- package/dist/chunk-JQXZUEEL.js +0 -525
- package/dist/chunk-JQXZUEEL.js.map +0 -1
- package/dist/chunk-SAY24E6V.js +0 -309
- package/dist/chunk-SAY24E6V.js.map +0 -1
package/dist/chunk-SAY24E6V.js
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { randomId, attachmentKindFor, ChatError } from './chunk-JQXZUEEL.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 { name: f.name, contentType: f.contentType, kind: f.meta?.kind ?? attachmentKindFor(f.contentType), meta: f.meta };
|
|
126
|
-
}
|
|
127
|
-
var memoryOutboxStore = () => {
|
|
128
|
-
let saved = [];
|
|
129
|
-
return { load: async () => saved, save: async (q) => void (saved = q) };
|
|
130
|
-
};
|
|
131
|
-
function createOutbox(opts) {
|
|
132
|
-
const store = opts.store ?? memoryOutboxStore();
|
|
133
|
-
let queue = [];
|
|
134
|
-
let loaded = false;
|
|
135
|
-
const running = /* @__PURE__ */ new Set();
|
|
136
|
-
const uploaded = /* @__PURE__ */ new Map();
|
|
137
|
-
const newId = opts.newId ?? randomId;
|
|
138
|
-
const saveable = (q) => ({
|
|
139
|
-
...q,
|
|
140
|
-
files: q.files.filter((f) => typeof f.data === "string"),
|
|
141
|
-
pendingFiles: q.files.map((f) => ({ ...describe(f), lost: typeof f.data !== "string" }))
|
|
142
|
-
});
|
|
143
|
-
const emit = () => {
|
|
144
|
-
opts.onChange?.([...queue]);
|
|
145
|
-
void store.save(queue.map(saveable));
|
|
146
|
-
};
|
|
147
|
-
async function persist(q) {
|
|
148
|
-
if (!opts.persistFile) return;
|
|
149
|
-
for (let i = 0; i < q.files.length; i++) {
|
|
150
|
-
const f = q.files[i];
|
|
151
|
-
if (typeof f.data === "string") continue;
|
|
152
|
-
try {
|
|
153
|
-
q.files[i] = { ...f, data: await opts.persistFile(f) };
|
|
154
|
-
} catch {
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async function ensureLoaded() {
|
|
159
|
-
if (loaded) return;
|
|
160
|
-
loaded = true;
|
|
161
|
-
const saved = await store.load();
|
|
162
|
-
queue = saved.map((q) => {
|
|
163
|
-
const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;
|
|
164
|
-
return {
|
|
165
|
-
...q,
|
|
166
|
-
status: q.status === "sending" || lost > 0 ? "failed" : q.status,
|
|
167
|
-
error: lost > 0 ? "Attach the file again to send" : q.error ?? "Not sent",
|
|
168
|
-
files: q.files ?? [],
|
|
169
|
-
attempts: q.attempts ?? 0,
|
|
170
|
-
lostFiles: lost
|
|
171
|
-
};
|
|
172
|
-
});
|
|
173
|
-
if (queue.length) opts.onChange?.([...queue]);
|
|
174
|
-
}
|
|
175
|
-
async function uploadFile(conversationId, file) {
|
|
176
|
-
if (opts.upload) return opts.upload(conversationId, file);
|
|
177
|
-
const data = typeof file.data === "string" ? await (opts.readFile ?? notReadable)(file.data) : file.data;
|
|
178
|
-
return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType });
|
|
179
|
-
}
|
|
180
|
-
async function run(conversationId) {
|
|
181
|
-
if (running.has(conversationId)) return;
|
|
182
|
-
running.add(conversationId);
|
|
183
|
-
try {
|
|
184
|
-
for (; ; ) {
|
|
185
|
-
const next = queue.find((q) => q.input.conversationId === conversationId && q.status === "sending");
|
|
186
|
-
if (!next) break;
|
|
187
|
-
if ((next.lostFiles ?? 0) > 0) {
|
|
188
|
-
next.status = "failed";
|
|
189
|
-
next.error = "Attach the file again to send";
|
|
190
|
-
emit();
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
const attachments = [...next.input.attachments ?? []];
|
|
195
|
-
for (let i = 0; i < next.files.length; i++) {
|
|
196
|
-
const file = next.files[i];
|
|
197
|
-
const key = `${next.clientId}:${i}`;
|
|
198
|
-
let path = uploaded.get(key);
|
|
199
|
-
if (!path) {
|
|
200
|
-
path = await uploadFile(conversationId, file);
|
|
201
|
-
uploaded.set(key, path);
|
|
202
|
-
}
|
|
203
|
-
attachments.push({
|
|
204
|
-
...file.meta,
|
|
205
|
-
kind: file.meta?.kind ?? attachmentKindFor(file.contentType),
|
|
206
|
-
path,
|
|
207
|
-
name: file.meta?.name ?? file.name,
|
|
208
|
-
mime: file.contentType
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
const message = await opts.client.send({ ...next.input, attachments });
|
|
212
|
-
queue = queue.filter((q) => q.clientId !== next.clientId);
|
|
213
|
-
for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);
|
|
214
|
-
emit();
|
|
215
|
-
opts.onSent?.(message, next.clientId);
|
|
216
|
-
} catch (e) {
|
|
217
|
-
const err = e instanceof ChatError ? e : new ChatError(e instanceof Error ? e.message : String(e), void 0, "send");
|
|
218
|
-
next.attempts += 1;
|
|
219
|
-
next.status = "failed";
|
|
220
|
-
next.error = err.forbidden ? "You can't post here" : err.transient ? "Not sent. Check your connection." : err.message;
|
|
221
|
-
emit();
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} finally {
|
|
226
|
-
running.delete(conversationId);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
/** Queues a message; it shows at once and sends in order. Returns its clientId. */
|
|
231
|
-
async send(input, files = []) {
|
|
232
|
-
await ensureLoaded();
|
|
233
|
-
const clientId = input.clientId ?? newId();
|
|
234
|
-
const previews = {};
|
|
235
|
-
files.forEach((f, i) => {
|
|
236
|
-
if (f.previewUrl) previews[i] = f.previewUrl;
|
|
237
|
-
});
|
|
238
|
-
const queued = {
|
|
239
|
-
clientId,
|
|
240
|
-
input: { ...input, clientId },
|
|
241
|
-
senderId: opts.userId,
|
|
242
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
243
|
-
status: "sending",
|
|
244
|
-
previews,
|
|
245
|
-
files: [...files],
|
|
246
|
-
pendingFiles: files.map(describe),
|
|
247
|
-
attempts: 0
|
|
248
|
-
};
|
|
249
|
-
queue.push(queued);
|
|
250
|
-
opts.onChange?.([...queue]);
|
|
251
|
-
await persist(queued);
|
|
252
|
-
emit();
|
|
253
|
-
void run(input.conversationId);
|
|
254
|
-
return clientId;
|
|
255
|
-
},
|
|
256
|
-
async retry(clientId) {
|
|
257
|
-
await ensureLoaded();
|
|
258
|
-
const q = queue.find((x) => x.clientId === clientId);
|
|
259
|
-
if (!q) return;
|
|
260
|
-
for (const x of queue) if (x.input.conversationId === q.input.conversationId && x.status === "failed") x.status = "sending";
|
|
261
|
-
emit();
|
|
262
|
-
void run(q.input.conversationId);
|
|
263
|
-
},
|
|
264
|
-
/** Gives a message whose files were lost on a relaunch its files again, and sends it. */
|
|
265
|
-
async reattach(clientId, files) {
|
|
266
|
-
await ensureLoaded();
|
|
267
|
-
const q = queue.find((x) => x.clientId === clientId);
|
|
268
|
-
if (!q) return;
|
|
269
|
-
q.files = [...files];
|
|
270
|
-
q.pendingFiles = files.map(describe);
|
|
271
|
-
q.lostFiles = 0;
|
|
272
|
-
await persist(q);
|
|
273
|
-
q.status = "sending";
|
|
274
|
-
emit();
|
|
275
|
-
void run(q.input.conversationId);
|
|
276
|
-
},
|
|
277
|
-
/** Retries every failed message (after the connection comes back). */
|
|
278
|
-
async retryAll() {
|
|
279
|
-
await ensureLoaded();
|
|
280
|
-
const conversations = /* @__PURE__ */ new Set();
|
|
281
|
-
for (const x of queue) {
|
|
282
|
-
if (x.status === "failed" && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== "string" || opts.readFile || opts.upload)) {
|
|
283
|
-
x.status = "sending";
|
|
284
|
-
conversations.add(x.input.conversationId);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
emit();
|
|
288
|
-
for (const c of conversations) void run(c);
|
|
289
|
-
},
|
|
290
|
-
async discard(clientId) {
|
|
291
|
-
await ensureLoaded();
|
|
292
|
-
queue = queue.filter((q) => q.clientId !== clientId);
|
|
293
|
-
emit();
|
|
294
|
-
},
|
|
295
|
-
pending(conversationId) {
|
|
296
|
-
return queue.filter((q) => !conversationId || q.input.conversationId === conversationId).map((q) => ({ ...q, files: q.pendingFiles }));
|
|
297
|
-
},
|
|
298
|
-
/** A new client id, made the way this outbox makes them. */
|
|
299
|
-
newId,
|
|
300
|
-
load: ensureLoaded
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
async function notReadable() {
|
|
304
|
-
throw new ChatError("This file can't be read on this device", "22023", "upload");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages };
|
|
308
|
-
//# sourceMappingURL=chunk-SAY24E6V.js.map
|
|
309
|
-
//# sourceMappingURL=chunk-SAY24E6V.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/thread.ts","../src/core/outbox.ts"],"names":[],"mappings":"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","file":"chunk-SAY24E6V.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. Storage is the platform's (memory by default;\n * localStorage on web, MMKV or SQLite on native), behind OutboxStore.\n */\nimport type { ChatClient } from \"./client\";\nimport { ChatError } from \"./client\";\nimport { attachmentKindFor, randomId } from \"./media\";\nimport type { AttachmentInput, ChatMessage, 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). */\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\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}\n\nfunction describe(f: LocalFile): PendingFile {\n return { name: f.name, contentType: f.contentType, kind: f.meta?.kind ?? attachmentKindFor(f.contentType), meta: f.meta };\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 /** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. */\n upload?: (conversationId: string, file: LocalFile) => Promise<string>;\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\n\nexport function createOutbox(opts: OutboxOptions) {\n const store = opts.store ?? memoryOutboxStore();\n let queue: QueuedMessage[] = [];\n let loaded = false;\n const running = new Set<string>();\n const uploaded = new Map<string, string>(); // clientId:index -> path\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 const saveable = (q: QueuedMessage): QueuedMessage => ({\n ...q,\n files: q.files.filter((f) => typeof f.data === \"string\"),\n pendingFiles: q.files.map((f) => ({ ...describe(f), lost: typeof f.data !== \"string\" })),\n });\n\n const emit = () => {\n opts.onChange?.([...queue]);\n void store.save(queue.map(saveable));\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 async function ensureLoaded() {\n if (loaded) return;\n loaded = true;\n const saved = await store.load();\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 queue = saved.map((q) => {\n const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;\n return {\n ...q,\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 (queue.length) opts.onChange?.([...queue]);\n }\n\n async function uploadFile(conversationId: string, file: LocalFile): Promise<string> {\n if (opts.upload) return opts.upload(conversationId, file);\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 });\n }\n\n async function run(conversationId: string) {\n if (running.has(conversationId)) return;\n running.add(conversationId);\n try {\n for (;;) {\n const next = queue.find((q) => q.input.conversationId === conversationId && q.status === \"sending\");\n if (!next) 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 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 const key = `${next.clientId}:${i}`;\n let path = uploaded.get(key);\n if (!path) {\n path = await uploadFile(conversationId, file);\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 const message = await opts.client.send({ ...next.input, attachments });\n queue = queue.filter((q) => q.clientId !== next.clientId);\n for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);\n emit();\n opts.onSent?.(message, next.clientId);\n } catch (e) {\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.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 }\n }\n } finally {\n running.delete(conversationId);\n }\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 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 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 behind it in that conversation.\n for (const x of queue) if (x.input.conversationId === q.input.conversationId && x.status === \"failed\") x.status = \"sending\";\n emit();\n void run(q.input.conversationId);\n },\n\n /** Gives a message whose files were lost on a relaunch its files again, and sends it. */\n async reattach(clientId: string, files: LocalFile[]) {\n await ensureLoaded();\n const q = queue.find((x) => x.clientId === clientId);\n if (!q) return;\n q.files = [...files];\n q.pendingFiles = files.map(describe);\n q.lostFiles = 0;\n await persist(q);\n q.status = \"sending\";\n emit();\n void run(q.input.conversationId);\n },\n\n /** Retries every failed message (after the connection comes back). */\n async retryAll() {\n await ensureLoaded();\n const conversations = new Set<string>();\n for (const x of queue) {\n if (x.status === \"failed\" && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== \"string\" || opts.readFile || opts.upload)) {\n x.status = \"sending\";\n conversations.add(x.input.conversationId);\n }\n }\n emit();\n for (const c of conversations) void run(c);\n },\n\n async discard(clientId: string) {\n await ensureLoaded();\n queue = queue.filter((q) => q.clientId !== clientId);\n emit();\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 throw new ChatError(\"This file can't be read on this device\", \"22023\", \"upload\");\n}\n"]}
|