@marktiderman/genesis-chat 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -13
- package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
- package/dist/chunk-5D5A7I4Q.js.map +1 -0
- package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
- package/dist/chunk-7S6HYNWW.js.map +1 -0
- package/dist/chunk-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-PZTOY2LY.js +576 -0
- package/dist/chunk-PZTOY2LY.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-DMa2j7kz.d.ts} +152 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1910 -1
- package/dist/native.js +4900 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B0y3phE0.d.ts +151 -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 +149 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +510 -21
- package/dist/server.js +863 -21
- package/dist/server.js.map +1 -1
- package/dist/thread-DWXHKPdw.d.ts +57 -0
- package/dist/web.d.ts +313 -10
- package/dist/web.js +1731 -60
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
- package/package.json +74 -6
- package/src/__tests__/coaching-core.test.ts +332 -0
- package/src/__tests__/coaching-react.test.tsx +248 -0
- package/src/__tests__/coaching-web.test.tsx +449 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +402 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fakes.ts +90 -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 +290 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +333 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +757 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +429 -0
- package/src/core/thread.ts +171 -0
- package/src/core/types.ts +311 -0
- package/src/index.ts +13 -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 +238 -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 +344 -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 +421 -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 +116 -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 +116 -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 +121 -0
- package/src/native/message-bubble.tsx +374 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +275 -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 +314 -0
- package/src/native/search-screen.tsx +168 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +620 -0
- package/src/native/topics.tsx +179 -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 +370 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +271 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +283 -0
- package/src/react/index.tsx +810 -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/handler.ts +32 -0
- package/src/server/index.ts +57 -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 +563 -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 +195 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +706 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +192 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +802 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +522 -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.map +0 -1
- package/dist/chunk-SAY24E6V.js.map +0 -1
|
@@ -1,3 +1,161 @@
|
|
|
1
|
+
// src/core/availability.ts
|
|
2
|
+
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
3
|
+
var SHORT = { mon: "Mon", tue: "Tue", wed: "Wed", thu: "Thu", fri: "Fri", sat: "Sat", sun: "Sun" };
|
|
4
|
+
function minutesOf(hhmm) {
|
|
5
|
+
if (typeof hhmm !== "string") return null;
|
|
6
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(hhmm.trim());
|
|
7
|
+
if (!m) return null;
|
|
8
|
+
const h = Number(m[1]);
|
|
9
|
+
const min = Number(m[2]);
|
|
10
|
+
if (h > 24 || min > 59 || h === 24 && min > 0) return null;
|
|
11
|
+
return h * 60 + min;
|
|
12
|
+
}
|
|
13
|
+
var MAX_RANGES_PER_DAY = 4;
|
|
14
|
+
function normalizeHours(raw) {
|
|
15
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
16
|
+
const src = raw;
|
|
17
|
+
const out = {};
|
|
18
|
+
let any = false;
|
|
19
|
+
for (const day of WEEKDAYS) {
|
|
20
|
+
if (!Object.prototype.hasOwnProperty.call(src, day)) continue;
|
|
21
|
+
const ranges = src[day];
|
|
22
|
+
if (!Array.isArray(ranges)) continue;
|
|
23
|
+
const kept = [];
|
|
24
|
+
for (const r of ranges) {
|
|
25
|
+
if (!Array.isArray(r) || r.length !== 2) continue;
|
|
26
|
+
const [from, to] = r;
|
|
27
|
+
if (typeof from !== "string" || typeof to !== "string" || minutesOf(from) == null || minutesOf(to) == null) continue;
|
|
28
|
+
kept.push([from, to]);
|
|
29
|
+
if (kept.length >= MAX_RANGES_PER_DAY) break;
|
|
30
|
+
}
|
|
31
|
+
if (kept.length) {
|
|
32
|
+
out[day] = kept;
|
|
33
|
+
any = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return any ? out : null;
|
|
37
|
+
}
|
|
38
|
+
function localClock(now, timeZone) {
|
|
39
|
+
let parts;
|
|
40
|
+
try {
|
|
41
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
42
|
+
timeZone: timeZone || void 0,
|
|
43
|
+
weekday: "short",
|
|
44
|
+
hour: "2-digit",
|
|
45
|
+
minute: "2-digit",
|
|
46
|
+
hourCycle: "h23"
|
|
47
|
+
}).formatToParts(now);
|
|
48
|
+
} catch {
|
|
49
|
+
parts = new Intl.DateTimeFormat("en-US", { weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(now);
|
|
50
|
+
}
|
|
51
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
52
|
+
const day = get("weekday").slice(0, 3).toLowerCase();
|
|
53
|
+
const hour = Number(get("hour")) % 24;
|
|
54
|
+
return { day: WEEKDAYS.includes(day) ? day : "mon", minutes: hour * 60 + Number(get("minute")) };
|
|
55
|
+
}
|
|
56
|
+
function isWithinWorkingHours(raw, timeZone, now = /* @__PURE__ */ new Date()) {
|
|
57
|
+
const hours = normalizeHours(raw);
|
|
58
|
+
if (!hours) return true;
|
|
59
|
+
const { day, minutes } = localClock(now, timeZone);
|
|
60
|
+
const yesterday = WEEKDAYS[(WEEKDAYS.indexOf(day) + 6) % 7];
|
|
61
|
+
for (const [from, to] of hours[day] ?? []) {
|
|
62
|
+
const a = minutesOf(from);
|
|
63
|
+
const b = minutesOf(to);
|
|
64
|
+
if (a == null || b == null) continue;
|
|
65
|
+
if (b > a ? minutes >= a && minutes < b : minutes >= a) return true;
|
|
66
|
+
}
|
|
67
|
+
for (const [from, to] of hours[yesterday] ?? []) {
|
|
68
|
+
const a = minutesOf(from);
|
|
69
|
+
const b = minutesOf(to);
|
|
70
|
+
if (a == null || b == null) continue;
|
|
71
|
+
if (b <= a && minutes < b) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function isAway(a, now = /* @__PURE__ */ new Date()) {
|
|
76
|
+
if (!a) return false;
|
|
77
|
+
if (a.awayUntil) return Date.parse(a.awayUntil) > now.getTime();
|
|
78
|
+
return Boolean(a.awayMessage?.trim());
|
|
79
|
+
}
|
|
80
|
+
function clock(hhmm) {
|
|
81
|
+
const total = minutesOf(hhmm) ?? 0;
|
|
82
|
+
const h = Math.floor(total / 60) % 24;
|
|
83
|
+
const m = total % 60;
|
|
84
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
85
|
+
return m ? `${h12}:${String(m).padStart(2, "0")}` : String(h12);
|
|
86
|
+
}
|
|
87
|
+
function hoursSummary(raw) {
|
|
88
|
+
const hours = normalizeHours(raw);
|
|
89
|
+
if (!hours) return "";
|
|
90
|
+
const key = (d) => (hours[d] ?? []).map(([a, b]) => `${clock(a)}\u2013${clock(b)}`).join(", ");
|
|
91
|
+
const runs = [];
|
|
92
|
+
for (const d of WEEKDAYS) {
|
|
93
|
+
const text = key(d);
|
|
94
|
+
if (!text) continue;
|
|
95
|
+
const last = runs[runs.length - 1];
|
|
96
|
+
if (last && last.text === text && WEEKDAYS.indexOf(last.to) === WEEKDAYS.indexOf(d) - 1) last.to = d;
|
|
97
|
+
else runs.push({ from: d, to: d, text });
|
|
98
|
+
}
|
|
99
|
+
return runs.map((r) => {
|
|
100
|
+
const days = r.from === r.to ? SHORT[r.from] : WEEKDAYS.indexOf(r.to) - WEEKDAYS.indexOf(r.from) === 1 ? `${SHORT[r.from]}, ${SHORT[r.to]}` : `${SHORT[r.from]}\u2013${SHORT[r.to]}`;
|
|
101
|
+
return `${days}, ${r.text}`;
|
|
102
|
+
}).join("; ");
|
|
103
|
+
}
|
|
104
|
+
function timeZoneLabel(timeZone, locale = "en-US") {
|
|
105
|
+
if (!timeZone) return "";
|
|
106
|
+
try {
|
|
107
|
+
const name = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "longGeneric" }).formatToParts(/* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value;
|
|
108
|
+
if (name && !/^GMT|^UTC/.test(name)) return name.replace(/ Time$/, "");
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return (timeZone.split("/").pop() ?? timeZone).replace(/_/g, " ");
|
|
112
|
+
}
|
|
113
|
+
function untilLabel(iso, now = /* @__PURE__ */ new Date(), locale, timeZone) {
|
|
114
|
+
const d = new Date(iso);
|
|
115
|
+
if (!Number.isFinite(d.getTime())) return "later";
|
|
116
|
+
try {
|
|
117
|
+
return untilIn(d, now, locale, timeZone || void 0);
|
|
118
|
+
} catch {
|
|
119
|
+
return untilIn(d, now, void 0, void 0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function untilIn(d, now, locale, tz) {
|
|
123
|
+
const dayKey = (x) => new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit" }).format(x);
|
|
124
|
+
if (dayKey(d) === dayKey(now)) return d.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit", timeZone: tz });
|
|
125
|
+
const days = (d.getTime() - now.getTime()) / 864e5;
|
|
126
|
+
if (days < 7) return d.toLocaleDateString(locale, { weekday: "long", timeZone: tz });
|
|
127
|
+
return d.toLocaleDateString(locale, { day: "numeric", month: "short", timeZone: tz });
|
|
128
|
+
}
|
|
129
|
+
function awayLine(a, name, opts = {}) {
|
|
130
|
+
if (!a) return null;
|
|
131
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
132
|
+
const message = a.awayMessage?.trim();
|
|
133
|
+
if (isAway(a, now)) {
|
|
134
|
+
if (a.awayUntil) return `Away until ${untilLabel(a.awayUntil, now, opts.locale, a.timeZone)}${message ? `: ${message}` : ""}`;
|
|
135
|
+
return `${name} is away${message ? `: ${message}` : ""}`;
|
|
136
|
+
}
|
|
137
|
+
const summary = hoursSummary(a.hours);
|
|
138
|
+
if (!summary) return null;
|
|
139
|
+
if (!opts.always && isWithinWorkingHours(a.hours, a.timeZone, now)) return null;
|
|
140
|
+
const zone = timeZoneLabel(a.timeZone, opts.locale);
|
|
141
|
+
return `${name} replies ${summary}${zone ? ` (${zone})` : ""}`;
|
|
142
|
+
}
|
|
143
|
+
function namesLine(names) {
|
|
144
|
+
if (names.length <= 1) return names[0] ?? "";
|
|
145
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
146
|
+
const rest = names.length - 2;
|
|
147
|
+
return `${names[0]}, ${names[1]} and ${rest === 1 ? "1 other" : `${rest} others`}`;
|
|
148
|
+
}
|
|
149
|
+
function awaySummary(people, opts = {}) {
|
|
150
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
151
|
+
const out = people.filter((p) => awayLine(p.availability, p.name, { ...opts, now, always: false }) !== null);
|
|
152
|
+
if (out.length === 0) return null;
|
|
153
|
+
if (out.length === 1) return awayLine(out[0].availability, out[0].name, { ...opts, now, always: false });
|
|
154
|
+
const away = out.filter((p) => isAway(p.availability, now)).length;
|
|
155
|
+
const what = away === out.length ? "away" : away === 0 ? "outside their hours" : "away or outside their hours";
|
|
156
|
+
return `${namesLine(out.map((p) => p.name))} are ${what}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
1
159
|
// src/core/media.ts
|
|
2
160
|
function randomId() {
|
|
3
161
|
const c = globalThis.crypto;
|
|
@@ -147,6 +305,14 @@ var ChatError = class extends Error {
|
|
|
147
305
|
get transient() {
|
|
148
306
|
return this.code === void 0 || this.code === "" || /^(08|57|53)/.test(this.code) || this.code === "PGRST000";
|
|
149
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Sending it again won't help: the database refused what was sent (not allowed 42501, bad
|
|
310
|
+
* data 22xxx such as 22023, a conflict 23xxx such as 23505, another refusal 42xxx, or a
|
|
311
|
+
* function's own `raise` P0001). A person may still retry by hand; nothing retries it alone.
|
|
312
|
+
*/
|
|
313
|
+
get permanent() {
|
|
314
|
+
return !this.transient && !this.rateLimited && /^(22|23|42|P0)/.test(this.code ?? "");
|
|
315
|
+
}
|
|
150
316
|
};
|
|
151
317
|
function must(operation, result) {
|
|
152
318
|
if (result.error) throw new ChatError(result.error.message, result.error.code, operation);
|
|
@@ -204,6 +370,7 @@ function toMessage(r) {
|
|
|
204
370
|
createdAt: String(r.created_at),
|
|
205
371
|
editedAt: str(r.edited_at),
|
|
206
372
|
deletedAt: str(r.deleted_at),
|
|
373
|
+
moderation: r.moderation === "hidden" || r.moderation === "flagged" ? r.moderation : null,
|
|
207
374
|
attachments,
|
|
208
375
|
reactions
|
|
209
376
|
};
|
|
@@ -256,6 +423,94 @@ function toInboxItem(r) {
|
|
|
256
423
|
} : null
|
|
257
424
|
};
|
|
258
425
|
}
|
|
426
|
+
function toTopic(r) {
|
|
427
|
+
return {
|
|
428
|
+
conversationId: String(r.conversation_id),
|
|
429
|
+
topic: String(r.topic),
|
|
430
|
+
title: String(r.title ?? r.topic),
|
|
431
|
+
position: Number(r.position ?? 0),
|
|
432
|
+
closedAt: str(r.closed_at),
|
|
433
|
+
createdBy: str(r.created_by),
|
|
434
|
+
createdAt: String(r.created_at)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function fromAttachmentArg(a) {
|
|
438
|
+
const out = { kind: String(a.kind ?? "file") };
|
|
439
|
+
const map = [
|
|
440
|
+
["path", "path"],
|
|
441
|
+
["provider", "provider"],
|
|
442
|
+
["providerId", "provider_id"],
|
|
443
|
+
["url", "url"],
|
|
444
|
+
["ref", "ref"],
|
|
445
|
+
["status", "status"],
|
|
446
|
+
["name", "name"],
|
|
447
|
+
["mime", "mime"],
|
|
448
|
+
["bytes", "bytes"],
|
|
449
|
+
["width", "width"],
|
|
450
|
+
["height", "height"],
|
|
451
|
+
["durationMs", "duration_ms"],
|
|
452
|
+
["waveform", "waveform"],
|
|
453
|
+
["posterPath", "poster_path"],
|
|
454
|
+
["thumbhash", "thumbhash"]
|
|
455
|
+
];
|
|
456
|
+
for (const [k, col] of map) if (a[col] != null) out[k] = a[col];
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function toScheduled(r) {
|
|
460
|
+
const sentMessageId = str(r.sent_message_id);
|
|
461
|
+
const cancelledAt = str(r.cancelled_at);
|
|
462
|
+
const error = str(r.error);
|
|
463
|
+
return {
|
|
464
|
+
id: String(r.id),
|
|
465
|
+
conversationId: String(r.conversation_id),
|
|
466
|
+
clientId: String(r.client_id),
|
|
467
|
+
body: String(r.body ?? ""),
|
|
468
|
+
attachments: (r.attachments ?? []).map(fromAttachmentArg),
|
|
469
|
+
replyToId: str(r.reply_to_id),
|
|
470
|
+
topic: str(r.topic),
|
|
471
|
+
mentions: r.mentions ?? [],
|
|
472
|
+
silent: Boolean(r.silent),
|
|
473
|
+
sendAt: String(r.send_at),
|
|
474
|
+
sentMessageId,
|
|
475
|
+
cancelledAt,
|
|
476
|
+
error,
|
|
477
|
+
createdAt: String(r.created_at),
|
|
478
|
+
status: sentMessageId ? "sent" : cancelledAt ? "cancelled" : error ? "failed" : "scheduled"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function toAvailability(r) {
|
|
482
|
+
return {
|
|
483
|
+
userId: String(r.user_id),
|
|
484
|
+
awayMessage: str(r.away_message),
|
|
485
|
+
awayUntil: str(r.away_until),
|
|
486
|
+
timeZone: str(r.time_zone),
|
|
487
|
+
// Someone else's hours: only well-formed days and ranges reach the renderers.
|
|
488
|
+
hours: normalizeHours(r.hours)
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function toSearchHit(r) {
|
|
492
|
+
return {
|
|
493
|
+
messageId: String(r.message_id),
|
|
494
|
+
conversationId: String(r.conversation_id),
|
|
495
|
+
seq: Number(r.seq),
|
|
496
|
+
senderId: str(r.sender_id),
|
|
497
|
+
createdAt: String(r.created_at),
|
|
498
|
+
snippet: String(r.snippet ?? ""),
|
|
499
|
+
inTranscript: Boolean(r.in_transcript)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function toLinkPreview(r) {
|
|
503
|
+
const image = str(r.image_url);
|
|
504
|
+
return {
|
|
505
|
+
url: String(r.url),
|
|
506
|
+
status: r.status ?? "ok",
|
|
507
|
+
title: str(r.title),
|
|
508
|
+
description: str(r.description),
|
|
509
|
+
imageUrl: image && /^https:\/\//i.test(image) ? image : null,
|
|
510
|
+
siteName: str(r.site_name),
|
|
511
|
+
fetchedAt: String(r.fetched_at)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
259
514
|
var MESSAGE_COLUMNS = "*, attachments(*), reactions(emoji, user_id)";
|
|
260
515
|
function createChatClient(supabase, options = {}) {
|
|
261
516
|
const db = () => supabase.schema(CHAT_SCHEMA);
|
|
@@ -277,20 +532,24 @@ function createChatClient(supabase, options = {}) {
|
|
|
277
532
|
}
|
|
278
533
|
return {
|
|
279
534
|
supabase,
|
|
535
|
+
/** Your conversations: one `kind`, several `kinds`, or all but `excludeKinds` (e.g. Messages without `support`). */
|
|
280
536
|
async inbox(opts = {}) {
|
|
281
537
|
const rows = await rpc("inbox", {
|
|
282
538
|
p_kind: opts.kind ?? null,
|
|
283
539
|
p_archived: opts.archived ?? false,
|
|
284
540
|
p_limit: opts.limit ?? 50,
|
|
285
|
-
p_before: opts.before ?? null
|
|
541
|
+
p_before: opts.before ?? null,
|
|
542
|
+
...opts.kinds?.length ? { p_kinds: opts.kinds } : {},
|
|
543
|
+
...opts.excludeKinds?.length ? { p_exclude_kinds: opts.excludeKinds } : {}
|
|
286
544
|
});
|
|
287
545
|
return rows.map(toInboxItem);
|
|
288
546
|
},
|
|
289
547
|
async observed(kind, opts = {}) {
|
|
290
548
|
return rpc("observed", { p_kind: kind, p_status: opts.status ?? null, p_limit: opts.limit ?? 50, p_before: opts.before ?? null });
|
|
291
549
|
},
|
|
292
|
-
|
|
293
|
-
|
|
550
|
+
/** Unread across your conversations (for a badge), leaving out `excludeKinds` to match an inbox that does. */
|
|
551
|
+
async unreadTotal(opts = {}) {
|
|
552
|
+
return rpc("unread_total", opts.excludeKinds?.length ? { p_exclude_kinds: opts.excludeKinds } : {});
|
|
294
553
|
},
|
|
295
554
|
async conversation(id) {
|
|
296
555
|
const row = must("conversation", await db().from("conversations").select("*").eq("id", id).maybeSingle());
|
|
@@ -423,6 +682,115 @@ function createChatClient(supabase, options = {}) {
|
|
|
423
682
|
async report(messageId, reason, note) {
|
|
424
683
|
return rpc("report_message", { p_message_id: messageId, p_reason: reason, p_note: note ?? null });
|
|
425
684
|
},
|
|
685
|
+
// ── Migration 0002 ──────────────────────────────────────────────────────
|
|
686
|
+
/** A conversation's topics, in order (position, then key). */
|
|
687
|
+
async topics(conversationId) {
|
|
688
|
+
const rows = must(
|
|
689
|
+
"topics",
|
|
690
|
+
await db().from("topics").select("*").eq("conversation_id", conversationId).order("position", { ascending: true }).order("topic", { ascending: true })
|
|
691
|
+
);
|
|
692
|
+
return rows.map(toTopic);
|
|
693
|
+
},
|
|
694
|
+
/** Adds a topic, or renames, reorders, closes (`closed: true`) or reopens it. Owners and admins. */
|
|
695
|
+
async setTopic(conversationId, topic, input = {}) {
|
|
696
|
+
await rpc("set_topic", {
|
|
697
|
+
p_conversation_id: conversationId,
|
|
698
|
+
p_topic: topic,
|
|
699
|
+
p_title: input.title ?? null,
|
|
700
|
+
p_position: input.position ?? null,
|
|
701
|
+
p_closed: input.closed ?? null
|
|
702
|
+
});
|
|
703
|
+
},
|
|
704
|
+
/** Schedules a message (a minute to a year ahead) and returns its id. Safe to retry with the same clientId. */
|
|
705
|
+
async schedule(input) {
|
|
706
|
+
const sendAt = typeof input.sendAt === "string" ? input.sendAt : input.sendAt.toISOString();
|
|
707
|
+
return rpc("schedule_message", {
|
|
708
|
+
p_conversation_id: input.conversationId,
|
|
709
|
+
p_client_id: input.clientId,
|
|
710
|
+
p_send_at: sendAt,
|
|
711
|
+
p_body: input.body ?? "",
|
|
712
|
+
p_attachments: (input.attachments ?? []).map(attachmentArg),
|
|
713
|
+
p_reply_to_id: input.replyToId ?? null,
|
|
714
|
+
p_topic: input.topic ?? null,
|
|
715
|
+
p_mentions: input.mentions ?? [],
|
|
716
|
+
p_silent: input.silent ?? false
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
/** Your messages still waiting to go (and ones that failed when due), soonest first; in one conversation or all. */
|
|
720
|
+
async scheduled(conversationId) {
|
|
721
|
+
let q = db().from("scheduled").select("*").is("sent_message_id", null).is("cancelled_at", null);
|
|
722
|
+
if (conversationId) q = q.eq("conversation_id", conversationId);
|
|
723
|
+
const rows = must("scheduled", await q.order("send_at", { ascending: true }).limit(200));
|
|
724
|
+
return rows.map(toScheduled);
|
|
725
|
+
},
|
|
726
|
+
async cancelScheduled(id) {
|
|
727
|
+
await rpc("cancel_scheduled", { p_id: id });
|
|
728
|
+
},
|
|
729
|
+
/** The away message and hours of a conversation's members who set them (not observers). */
|
|
730
|
+
async availabilityFor(conversationId) {
|
|
731
|
+
const rows = await rpc("availability_for", { p_conversation_id: conversationId });
|
|
732
|
+
return (rows ?? []).map(toAvailability);
|
|
733
|
+
},
|
|
734
|
+
/** Your own away message and hours, or null when never set. */
|
|
735
|
+
async myAvailability() {
|
|
736
|
+
const row = must("myAvailability", await db().from("availability").select("*").maybeSingle());
|
|
737
|
+
return row ? toAvailability(row) : null;
|
|
738
|
+
},
|
|
739
|
+
/** Replaces your away message and hours (a field left out is cleared). */
|
|
740
|
+
async setAvailability(input) {
|
|
741
|
+
const until = input.awayUntil ?? null;
|
|
742
|
+
await rpc("set_availability", {
|
|
743
|
+
p_away_message: input.awayMessage ?? null,
|
|
744
|
+
p_away_until: until,
|
|
745
|
+
p_time_zone: input.timeZone ?? null,
|
|
746
|
+
p_hours: input.hours ?? null
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
/**
|
|
750
|
+
* Sends one text message into each of these conversations where you may post (a coach to
|
|
751
|
+
* each client's direct conversation), so replies come back privately. Text only. Safe to
|
|
752
|
+
* retry with the same clientId. Returns the ids of the messages sent.
|
|
753
|
+
*/
|
|
754
|
+
async broadcast(conversationIds, clientId, body, opts = {}) {
|
|
755
|
+
const ids = await rpc("broadcast_message", {
|
|
756
|
+
p_conversation_ids: conversationIds,
|
|
757
|
+
p_client_id: clientId,
|
|
758
|
+
p_body: body,
|
|
759
|
+
p_attachments: [],
|
|
760
|
+
p_silent: opts.silent ?? false
|
|
761
|
+
});
|
|
762
|
+
return (ids ?? []).map(String);
|
|
763
|
+
},
|
|
764
|
+
/** Messages (and transcripts) you can read that match, newest first: in one conversation or all of yours. Two characters at least. */
|
|
765
|
+
async search(query, conversationId, opts = {}) {
|
|
766
|
+
if (query.trim().length < 2) return [];
|
|
767
|
+
const rows = await rpc("search_messages", {
|
|
768
|
+
p_query: query,
|
|
769
|
+
p_conversation_id: conversationId ?? null,
|
|
770
|
+
p_limit: opts.limit ?? 30
|
|
771
|
+
});
|
|
772
|
+
return (rows ?? []).map(toSearchHit);
|
|
773
|
+
},
|
|
774
|
+
/**
|
|
775
|
+
* The previews the server has fetched for the links in these messages, by message id (only
|
|
776
|
+
* `ok` ones; links not fetched yet are left out). Read through link_previews_for, one call
|
|
777
|
+
* per message (up to 6 at once), which answers only for messages you can read: the preview
|
|
778
|
+
* table itself is closed, since a URL can carry a secret.
|
|
779
|
+
*/
|
|
780
|
+
async linkPreviewsFor(messageIds) {
|
|
781
|
+
const ids = [...new Set(messageIds.filter(Boolean))].slice(0, 200);
|
|
782
|
+
const out = {};
|
|
783
|
+
let next = 0;
|
|
784
|
+
const worker = async () => {
|
|
785
|
+
while (next < ids.length) {
|
|
786
|
+
const id = ids[next++];
|
|
787
|
+
const rows = await rpc("link_previews_for", { p_message_id: id });
|
|
788
|
+
out[id] = (rows ?? []).map(toLinkPreview).filter((p) => p.status === "ok");
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
await Promise.all(Array.from({ length: Math.min(6, ids.length) }, worker));
|
|
792
|
+
return out;
|
|
793
|
+
},
|
|
426
794
|
/**
|
|
427
795
|
* Uploads a file into the conversation's folder for the signed-in person and
|
|
428
796
|
* returns its path, ready to attach. Resumable for big files is the native
|
|
@@ -468,8 +836,10 @@ function createChatClient(supabase, options = {}) {
|
|
|
468
836
|
return { unsubscribe: join(channel) };
|
|
469
837
|
},
|
|
470
838
|
/**
|
|
471
|
-
* A conversation's live hints, plus who is typing or recording. `
|
|
472
|
-
*
|
|
839
|
+
* A conversation's live hints, plus who is typing or recording. `onJoin` runs every
|
|
840
|
+
* time the channel joins, the first time included: the moment to fetch what arrived
|
|
841
|
+
* before the subscription was live. `onReconnect` (kept for older callers) runs on
|
|
842
|
+
* re-joins only.
|
|
473
843
|
*/
|
|
474
844
|
subscribeConversation(conversationId, me, handlers) {
|
|
475
845
|
let joinedOnce = false;
|
|
@@ -484,14 +854,18 @@ function createChatClient(supabase, options = {}) {
|
|
|
484
854
|
).on("broadcast", { event: "members" }, () => handlers.onSignal({ type: "members", conversationId })).on("broadcast", { event: "conversation" }, () => handlers.onSignal({ type: "conversation", conversationId })).on("broadcast", { event: "activity" }, ({ payload }) => {
|
|
485
855
|
if (payload?.userId && payload.userId !== me) handlers.onActivity?.({ userId: payload.userId, activity: payload.activity, at: Date.now() });
|
|
486
856
|
});
|
|
857
|
+
let joined = false;
|
|
487
858
|
const leave = join(channel, (status) => {
|
|
859
|
+
joined = status === "SUBSCRIBED";
|
|
488
860
|
if (status === "SUBSCRIBED") {
|
|
861
|
+
handlers.onJoin?.();
|
|
489
862
|
if (joinedOnce) handlers.onReconnect?.();
|
|
490
863
|
joinedOnce = true;
|
|
491
864
|
}
|
|
492
865
|
});
|
|
493
866
|
return {
|
|
494
867
|
signal(activity) {
|
|
868
|
+
if (!joined) return;
|
|
495
869
|
void channel.send({ type: "broadcast", event: "activity", payload: { userId: me, activity } });
|
|
496
870
|
},
|
|
497
871
|
unsubscribe: leave
|
|
@@ -520,6 +894,6 @@ function attachmentArg(a) {
|
|
|
520
894
|
};
|
|
521
895
|
}
|
|
522
896
|
|
|
523
|
-
export { ALLOWED_EXTENSIONS, CHAT_BUCKET, CHAT_LIMITS, CHAT_SCHEMA, ChatError, PLAYBACK_RATES, attachmentKindFor, conversationTopic, createChatClient, fileExtension, formatDuration, isAllowedExtension, nextPlaybackRate, randomId, recordingLimitMs, toAttachment, toMessage, userTopic, waveformFromMetering, waveformPeaks };
|
|
524
|
-
//# sourceMappingURL=chunk-
|
|
525
|
-
//# sourceMappingURL=chunk-
|
|
897
|
+
export { ALLOWED_EXTENSIONS, CHAT_BUCKET, CHAT_LIMITS, CHAT_SCHEMA, ChatError, MAX_RANGES_PER_DAY, PLAYBACK_RATES, WEEKDAYS, attachmentKindFor, awayLine, awaySummary, conversationTopic, createChatClient, fileExtension, formatDuration, hoursSummary, isAllowedExtension, isAway, isWithinWorkingHours, localClock, minutesOf, namesLine, nextPlaybackRate, normalizeHours, randomId, recordingLimitMs, timeZoneLabel, toAttachment, toAvailability, toLinkPreview, toMessage, toScheduled, toSearchHit, toTopic, untilLabel, userTopic, waveformFromMetering, waveformPeaks };
|
|
898
|
+
//# sourceMappingURL=chunk-7S6HYNWW.js.map
|
|
899
|
+
//# sourceMappingURL=chunk-7S6HYNWW.js.map
|