@voltro/plugin-chat 0.0.0-stage → 0.88.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +24492 -0
  2. package/LICENSE +61 -0
  3. package/README.md +516 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +16105 -0
  6. package/dist/admissions.entity.js +10 -0
  7. package/dist/channels.entity.js +19 -0
  8. package/dist/chat-app/events/chat.mentioned.event.ts +1 -0
  9. package/dist/chat-app/events/chat.messageChanged.event.ts +1 -0
  10. package/dist/chat-app/events/chat.typing.event.ts +1 -0
  11. package/dist/chat-app/schedules/chat.retention.cron.tsx +16 -0
  12. package/dist/chat-app/subscribers/chat.messages.subscribe.ts +4 -0
  13. package/dist/components.js +431 -0
  14. package/dist/contract.js +91 -0
  15. package/dist/errors.d.ts +13 -0
  16. package/dist/errors.js +20 -0
  17. package/dist/eventProducer.d.ts +18 -0
  18. package/dist/eventProducer.js +45 -0
  19. package/dist/events.d.ts +38 -0
  20. package/dist/events.js +35 -0
  21. package/dist/evidence.entity.js +24 -0
  22. package/dist/index.d.ts +623 -0
  23. package/dist/index.js +180 -0
  24. package/dist/inspect.js +37 -0
  25. package/dist/media.entity.js +14 -0
  26. package/dist/members.entity.js +21 -0
  27. package/dist/mentionDraft.js +20 -0
  28. package/dist/mentionNotifications.js +109 -0
  29. package/dist/messages.entity.js +27 -0
  30. package/dist/reportEvidence.js +15 -0
  31. package/dist/retention.entity.js +10 -0
  32. package/dist/rpc.d.ts +295 -0
  33. package/dist/rpc.js +266 -0
  34. package/dist/rpcRoutes.js +96 -0
  35. package/dist/server.d.ts +200 -0
  36. package/dist/server.js +6 -0
  37. package/dist/service.js +1411 -0
  38. package/dist/strategies.d.ts +148 -0
  39. package/dist/strategies.js +89 -0
  40. package/dist/testing.d.ts +139 -0
  41. package/dist/testing.js +110 -0
  42. package/dist/typing.js +87 -0
  43. package/dist/typingInvalidation.js +85 -0
  44. package/dist/typingLease.js +58 -0
  45. package/dist/typingPublisher.js +58 -0
  46. package/dist/useChatTypingPublisher.js +32 -0
  47. package/dist/useTyping.js +23 -0
  48. package/dist/web.d.ts +354 -0
  49. package/dist/web.js +18 -0
  50. package/package.json +98 -4
@@ -0,0 +1,10 @@
1
+ import { boolean as e, id as t, integer as n, table as r, text as i } from "@voltro/database";
2
+ // src/admissions.entity.ts
3
+ var a = r("_voltro_chat_admissions", {
4
+ id: t({ prefix: "cadm" }),
5
+ tenantId: i().maxLength(256).externalId().serverOnly(),
6
+ subjectId: i().maxLength(256).nullable().externalId().sensitive("pii").serverOnly(),
7
+ admission: n().serverOnly(),
8
+ erased: e().serverOnly()
9
+ }).index(["tenantId", "subjectId"]);
10
+ export { a as chatAdmissionsTable };
@@ -0,0 +1,19 @@
1
+ import { CHANNELS_TABLE as e, CHANNEL_KINDS as t } from "./contract.js";
2
+ import { boolean as n, id as r, integer as i, json as a, table as o, text as s, timestamp as c } from "@voltro/database";
3
+ // src/channels.entity.ts
4
+ var l = o(e, {
5
+ id: r({ prefix: "cchan" }),
6
+ tenantId: s().maxLength(256).externalId().sensitive("pii"),
7
+ kind: s().oneOf(t).safe(),
8
+ refId: s().maxLength(256).sensitive("pii"),
9
+ name: s().sensitive("pii"),
10
+ slowModeMs: i().safe(),
11
+ retentionMs: i().nullable().safe(),
12
+ maxMembers: i().safe(),
13
+ frozen: n().safe(),
14
+ createdAt: c().safe(),
15
+ admission: i().serverOnly(),
16
+ creationKey: s().maxLength(64).serverOnly(),
17
+ metadata: a().sensitive("pii")
18
+ }).index(["tenantId", "kind"]);
19
+ export { l as chatChannelsTable };
@@ -0,0 +1 @@
1
+ export { mentioned as default } from '@voltro/plugin-chat/events'
@@ -0,0 +1 @@
1
+ export { messageChanged as default } from '@voltro/plugin-chat/events'
@@ -0,0 +1 @@
1
+ export { typing as default } from '@voltro/plugin-chat/events'
@@ -0,0 +1,16 @@
1
+ import { Effect } from 'effect'
2
+ import { defineSchedule } from '@voltro/runtime'
3
+ import { systemSubject } from '@voltro/protocol'
4
+ import { CHAT_RETENTION_RUN_BATCHES } from '@voltro/plugin-chat'
5
+ import { ChatAdministration } from '@voltro/plugin-chat/server'
6
+ /** Bounded app sweep; SQL owns progress across workers, retries and process replacement. */
7
+ export const runChatRetention = () => Effect.gen(function* () {
8
+ const chat = yield* ChatAdministration
9
+ for (let batch = 0; batch < CHAT_RETENTION_RUN_BATCHES; batch++) {
10
+ const result = yield* chat.prune(systemSubject('chat.retention'))
11
+ if (result.cycleComplete) break
12
+ }
13
+ // Pruned and erased media rows release their storage blobs outside SQL, one bounded step per run.
14
+ yield* chat.sweepMedia(systemSubject('chat.retention'))
15
+ })
16
+ export default defineSchedule({ name: 'chat.pruneHistory', cron: '* * * * *', timezone: 'UTC', onOverlap: 'skip', backfill: 'latest', handler: runChatRetention })
@@ -0,0 +1,4 @@
1
+ import { defineSubscriber } from '@voltro/runtime'
2
+ import { chatEventTenant, publishChatChange } from '@voltro/plugin-chat/events/server'
3
+ export default defineSubscriber({ table: '_voltro_chat_messages', on: ['insert','update','delete'], once: true,
4
+ eventTenant: chatEventTenant, handler: publishChatChange })
@@ -0,0 +1,431 @@
1
+ import { insertMentionDraft as e, mentionDraftToken as t } from "./mentionDraft.js";
2
+ import { useEffect as n, useId as r, useLayoutEffect as i, useMemo as a, useRef as o, useState as s } from "react";
3
+ import { Button as c, Textarea as l } from "@voltro/ui-shadcn";
4
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
5
+ // src/components.tsx
6
+ var f = {
7
+ en: {
8
+ title: "Chat",
9
+ empty: "No messages yet. Start the conversation.",
10
+ loading: "Loading messages…",
11
+ failed: "Chat could not be updated. Check the history before retrying.",
12
+ deleted: "Message deleted",
13
+ unknown: "Player unavailable",
14
+ edited: "Edited",
15
+ flagged: "Flagged for moderation",
16
+ draft: "Message",
17
+ send: "Send message",
18
+ sending: "Sending…",
19
+ older: "Load older messages",
20
+ report: "Report message",
21
+ unread: "unread messages",
22
+ more: "or more",
23
+ choose: "Choose a channel",
24
+ media: "Image",
25
+ openMedia: "Open image"
26
+ },
27
+ de: {
28
+ title: "Chat",
29
+ empty: "Noch keine Nachrichten. Starte das Gespräch.",
30
+ loading: "Nachrichten werden geladen…",
31
+ failed: "Chat konnte nicht aktualisiert werden. Prüfe die Historie vor einem erneuten Versuch.",
32
+ deleted: "Nachricht gelöscht",
33
+ unknown: "Spieler nicht verfügbar",
34
+ edited: "Bearbeitet",
35
+ flagged: "Zur Moderation markiert",
36
+ draft: "Nachricht",
37
+ send: "Nachricht senden",
38
+ sending: "Wird gesendet…",
39
+ older: "Ältere Nachrichten laden",
40
+ report: "Nachricht melden",
41
+ unread: "ungelesene Nachrichten",
42
+ more: "oder mehr",
43
+ choose: "Wähle einen Kanal",
44
+ media: "Bild",
45
+ openMedia: "Bild öffnen"
46
+ }
47
+ }, p = ({ message: e, author: t, labels: r, locale: i, position: a, total: s, onHeight: l, onReport: p, onOpenMedia: m, onFocused: h }) => {
48
+ let g = o(null);
49
+ return n(() => {
50
+ let t = g.current;
51
+ if (!t) return;
52
+ let n = () => {
53
+ let n = Math.ceil(t.getBoundingClientRect().height);
54
+ n > 0 && l(e.id, n);
55
+ };
56
+ if (n(), typeof ResizeObserver > "u") return;
57
+ let r = new ResizeObserver(n);
58
+ return r.observe(t), () => r.disconnect();
59
+ }, [e.id, l]), /* @__PURE__ */ d("li", {
60
+ ref: g,
61
+ tabIndex: 0,
62
+ "data-message-id": e.id,
63
+ "aria-posinset": a,
64
+ "aria-setsize": s,
65
+ onFocus: () => h(e.id),
66
+ className: "min-w-0 space-y-2 border-b border-border px-4 py-4 outline-offset-[-2px] focus-visible:outline-2 focus-visible:outline-ring",
67
+ children: [
68
+ /* @__PURE__ */ d("div", {
69
+ className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm",
70
+ children: [
71
+ /* @__PURE__ */ u("span", {
72
+ className: "break-words font-medium",
73
+ children: e.authorId === null || !t ? r.unknown : t.displayName
74
+ }),
75
+ /* @__PURE__ */ u("time", {
76
+ dateTime: new Date(e.createdAt).toISOString(),
77
+ className: "text-muted-foreground tabular-nums",
78
+ children: new Intl.DateTimeFormat(i, {
79
+ dateStyle: "short",
80
+ timeStyle: "short"
81
+ }).format(e.createdAt)
82
+ }),
83
+ e.editedAt === null ? null : /* @__PURE__ */ u("span", {
84
+ className: "text-muted-foreground",
85
+ children: r.edited
86
+ })
87
+ ]
88
+ }),
89
+ /* @__PURE__ */ u("p", {
90
+ className: "max-w-prose whitespace-pre-wrap break-words text-sm leading-relaxed",
91
+ children: e.deletedAt === null ? e.body : r.deleted
92
+ }),
93
+ e.kind === "media" && e.deletedAt === null ? /* @__PURE__ */ u("p", {
94
+ className: "text-sm text-muted-foreground",
95
+ children: r.media ?? f.en.media
96
+ }) : null,
97
+ e.kind === "media" && e.deletedAt === null && m ? /* @__PURE__ */ u(c, {
98
+ type: "button",
99
+ variant: "outline",
100
+ size: "sm",
101
+ onClick: () => m(e.id),
102
+ children: r.openMedia ?? f.en.openMedia
103
+ }) : null,
104
+ e.flagged ? /* @__PURE__ */ u("p", {
105
+ className: "text-sm text-muted-foreground",
106
+ children: r.flagged
107
+ }) : null,
108
+ p && e.deletedAt === null ? /* @__PURE__ */ u(c, {
109
+ type: "button",
110
+ variant: "ghost",
111
+ size: "sm",
112
+ onClick: () => p(e.id),
113
+ children: r.report
114
+ }) : null
115
+ ]
116
+ });
117
+ }, m = ({ messages: e, authors: t, labels: r = f.en, locale: i = "en", loading: c = !1, onReport: l, onOpenMedia: d }) => {
118
+ let m = o(null), [h, g] = s({
119
+ top: 0,
120
+ height: 480
121
+ }), [_, v] = s(() => /* @__PURE__ */ new Map()), [y, b] = s(null), x = a(() => [...e].sort((e, t) => e.createdAt - t.createdAt || (e.id < t.id ? -1 : +(e.id > t.id))), [e]), S = a(() => new Map(t.map((e) => [e.subjectId, e])), [t]), C = a(() => (e, t) => v((n) => {
122
+ if (n.get(e) === t) return n;
123
+ let r = new Map(n);
124
+ return r.set(e, t), r;
125
+ }), []);
126
+ n(() => {
127
+ let e = m.current;
128
+ if (!e) return;
129
+ let t = () => g({
130
+ top: e.scrollTop,
131
+ height: e.clientHeight || 480
132
+ });
133
+ if (t(), typeof ResizeObserver > "u") return;
134
+ let n = new ResizeObserver(t);
135
+ return n.observe(e), () => n.disconnect();
136
+ }, []);
137
+ let w = [0];
138
+ for (let e of x) w.push(w[w.length - 1] + (_.get(e.id) ?? 112));
139
+ let T = [], E = 0;
140
+ return x.forEach((e, t) => {
141
+ let n = w[t], a = w[t + 1];
142
+ (a >= h.top - 336 && n <= h.top + h.height + 336 || e.id === y) && (n > E && T.push(/* @__PURE__ */ u("li", {
143
+ "aria-hidden": "true",
144
+ role: "presentation",
145
+ style: { height: n - E }
146
+ }, `space-${e.id}`)), T.push(/* @__PURE__ */ u(p, {
147
+ message: e,
148
+ author: e.authorId === null ? void 0 : S.get(e.authorId),
149
+ labels: r,
150
+ locale: i,
151
+ position: t + 1,
152
+ total: x.length,
153
+ onHeight: C,
154
+ onFocused: b,
155
+ onReport: l,
156
+ onOpenMedia: d
157
+ }, e.id)), E = a);
158
+ }), w.at(-1) > E && T.push(/* @__PURE__ */ u("li", {
159
+ "aria-hidden": "true",
160
+ role: "presentation",
161
+ style: { height: w.at(-1) - E }
162
+ }, "space-end")), /* @__PURE__ */ u("div", {
163
+ ref: m,
164
+ tabIndex: 0,
165
+ "aria-label": r.title,
166
+ "aria-busy": c,
167
+ className: "max-h-[min(60dvh,40rem)] min-h-48 overflow-y-auto rounded-md border border-border",
168
+ onScroll: (e) => g({
169
+ top: e.currentTarget.scrollTop,
170
+ height: e.currentTarget.clientHeight || 480
171
+ }),
172
+ onBlur: (e) => {
173
+ e.currentTarget.contains(e.relatedTarget) || b(null);
174
+ },
175
+ children: x.length === 0 ? /* @__PURE__ */ u("p", {
176
+ className: "px-4 py-8 text-sm text-muted-foreground",
177
+ role: c ? "status" : void 0,
178
+ children: c ? r.loading : r.empty
179
+ }) : /* @__PURE__ */ u("ol", {
180
+ className: "m-0 list-none p-0",
181
+ children: T
182
+ })
183
+ });
184
+ }, h = {
185
+ en: {
186
+ typing: "is typing…",
187
+ peopleTyping: "players are typing…",
188
+ someoneTyping: "Someone is typing…"
189
+ },
190
+ de: {
191
+ typing: "schreibt…",
192
+ peopleTyping: "Spieler schreiben…",
193
+ someoneTyping: "Jemand schreibt…"
194
+ }
195
+ }, g = ({ subjects: e, authors: t, labels: n = h.en, locale: r = "en" }) => {
196
+ let i = [...new Set(e.slice(0, 128))], a = i.length === 1 ? t.find((e) => e.subjectId === i[0]) : void 0, o = i.length === 0 ? "" : i.length > 1 ? `${new Intl.NumberFormat(r).format(i.length)} ${n.peopleTyping}` : a ? `${a.displayName} ${n.typing}` : n.someoneTyping;
197
+ return /* @__PURE__ */ u("p", {
198
+ role: "status",
199
+ "aria-live": "polite",
200
+ "aria-atomic": "true",
201
+ className: "min-h-5 break-words text-sm text-muted-foreground",
202
+ children: o
203
+ });
204
+ }, _ = {
205
+ en: {
206
+ title: "Mention a player",
207
+ empty: "No matching players on this page.",
208
+ loading: "Loading players…",
209
+ more: "Load more players",
210
+ help: "Use Tab or Arrow Down to choose a player. Escape closes suggestions."
211
+ },
212
+ de: {
213
+ title: "Person erwähnen",
214
+ empty: "Keine passende Person auf dieser Seite.",
215
+ loading: "Personen werden geladen…",
216
+ more: "Weitere Personen laden",
217
+ help: "Mit Tab oder Pfeil nach unten eine Person wählen. Escape schließt Vorschläge."
218
+ }
219
+ }, v = ({ value: n, onChange: a, onSend: p, onTyping: m, mentionCandidates: h, mentionLabels: g = _.en, mentionsLoading: v = !1, onLoadMoreMentions: y, pending: b = !1, disabled: x = !1, error: S, labels: C = f.en, maxBodyLength: w = 2e3 }) => {
220
+ let T = r(), E = [...n].length, D = x || b || !n.trim() || E > w, O = o(null), k = o(null), [A, j] = s({
221
+ start: 0,
222
+ end: 0
223
+ }), [M, N] = s(!1), [P, F] = s(!1), I = o(null), L = !x && !b && M && !P && h !== void 0 ? t(n, A.start, A.end) : null, R = L === null || v ? [] : h.slice(0, 128).filter((t, r, i) => i.findIndex((e) => e.subjectId === t.subjectId) === r && t.displayName.normalize("NFKC").toLowerCase().startsWith(L.prefix.normalize("NFKC").toLowerCase()) && e(n, L, t.displayName, w) !== null).slice(0, 8), z = (e) => j({
224
+ start: e.selectionStart,
225
+ end: e.selectionEnd
226
+ });
227
+ i(() => {
228
+ let e = I.current;
229
+ e && e.body === n ? (I.current = null, O.current?.focus(), O.current?.setSelectionRange(e.caret, e.caret), j({
230
+ start: e.caret,
231
+ end: e.caret
232
+ })) : e && (I.current = null);
233
+ }, [n]);
234
+ let B = o(m);
235
+ return i(() => {
236
+ B.current = m;
237
+ }), i(() => {
238
+ (x || b || !n.trim()) && B.current?.(!1);
239
+ }, [
240
+ x,
241
+ b,
242
+ n
243
+ ]), i(() => () => {
244
+ B.current?.(!1);
245
+ }, []), /* @__PURE__ */ d("form", {
246
+ className: "space-y-3",
247
+ "aria-busy": b,
248
+ onBlur: (e) => {
249
+ e.currentTarget.contains(e.relatedTarget) || (N(!1), F(!0));
250
+ },
251
+ onKeyDown: (e) => {
252
+ e.key === "Escape" && L && (e.preventDefault(), F(!0), O.current?.focus());
253
+ },
254
+ onSubmit: (e) => {
255
+ e.preventDefault(), D || (B.current?.(!1), p(n));
256
+ },
257
+ children: [
258
+ /* @__PURE__ */ u("label", {
259
+ htmlFor: T,
260
+ className: "block text-sm font-medium",
261
+ children: C.draft
262
+ }),
263
+ /* @__PURE__ */ u(l, {
264
+ ref: O,
265
+ id: T,
266
+ value: n,
267
+ rows: 3,
268
+ readOnly: x || b,
269
+ "aria-describedby": `${T}-count${L ? ` ${T}-mention-help` : ""}`,
270
+ ...L ? { "aria-controls": `${T}-mentions` } : {},
271
+ onFocus: (e) => {
272
+ N(!0), z(e.currentTarget);
273
+ },
274
+ onSelect: (e) => z(e.currentTarget),
275
+ onKeyUp: (e) => z(e.currentTarget),
276
+ onKeyDown: (e) => {
277
+ e.key === "ArrowDown" && L && R.length && !e.nativeEvent.isComposing && (e.preventDefault(), k.current?.querySelector("button")?.focus());
278
+ },
279
+ onBlur: () => B.current?.(!1),
280
+ onChange: (e) => {
281
+ if (!x && !b) {
282
+ let t = e.target.value;
283
+ z(e.target), F(!1), a(t), B.current?.(t.trim().length > 0);
284
+ }
285
+ }
286
+ }),
287
+ L ? /* @__PURE__ */ d("div", {
288
+ ref: k,
289
+ id: `${T}-mentions`,
290
+ role: "group",
291
+ "aria-labelledby": `${T}-mention-title`,
292
+ className: "space-y-2 rounded-md border border-border p-3",
293
+ children: [
294
+ /* @__PURE__ */ u("p", {
295
+ id: `${T}-mention-title`,
296
+ className: "text-sm font-medium",
297
+ children: g.title
298
+ }),
299
+ /* @__PURE__ */ u("p", {
300
+ id: `${T}-mention-help`,
301
+ className: "text-sm text-muted-foreground",
302
+ children: g.help
303
+ }),
304
+ v ? /* @__PURE__ */ u("p", {
305
+ role: "status",
306
+ className: "text-sm text-muted-foreground",
307
+ children: g.loading
308
+ }) : R.length === 0 ? /* @__PURE__ */ u("p", {
309
+ role: "status",
310
+ className: "text-sm text-muted-foreground",
311
+ children: g.empty
312
+ }) : null,
313
+ /* @__PURE__ */ u("div", {
314
+ className: "flex flex-wrap gap-2",
315
+ children: R.map((t) => /* @__PURE__ */ u(c, {
316
+ type: "button",
317
+ variant: "outline",
318
+ className: "max-w-full whitespace-normal break-words",
319
+ onClick: () => {
320
+ let r = e(n, L, t.displayName, w);
321
+ x || b || !r || (I.current = r, F(!0), a(r.body), B.current?.(!0));
322
+ },
323
+ children: t.displayName
324
+ }, t.subjectId))
325
+ }),
326
+ y ? /* @__PURE__ */ u(c, {
327
+ type: "button",
328
+ variant: "ghost",
329
+ disabled: v,
330
+ onClick: y,
331
+ children: g.more
332
+ }) : null
333
+ ]
334
+ }) : null,
335
+ /* @__PURE__ */ d("div", {
336
+ className: "flex flex-wrap items-center justify-between gap-3",
337
+ children: [/* @__PURE__ */ d("span", {
338
+ id: `${T}-count`,
339
+ className: "text-sm text-muted-foreground tabular-nums",
340
+ children: [
341
+ E,
342
+ " / ",
343
+ w
344
+ ]
345
+ }), /* @__PURE__ */ u(c, {
346
+ type: "submit",
347
+ "aria-disabled": D,
348
+ children: b ? C.sending : C.send
349
+ })]
350
+ }),
351
+ S === void 0 ? null : /* @__PURE__ */ u("p", {
352
+ role: "alert",
353
+ className: "text-sm text-destructive",
354
+ children: C.failed
355
+ })
356
+ ]
357
+ });
358
+ }, y = ({ channels: e, selectedId: t, onSelect: n, unread: r = [], labels: i = f.en, disabled: a = !1 }) => /* @__PURE__ */ u("nav", {
359
+ "aria-label": i.choose,
360
+ className: "flex flex-wrap gap-2",
361
+ children: e.map((e) => {
362
+ let o = r.find((t) => t.channelId === e.id);
363
+ return /* @__PURE__ */ d(c, {
364
+ type: "button",
365
+ variant: t === e.id ? "default" : "outline",
366
+ "aria-current": t === e.id ? "page" : void 0,
367
+ "aria-disabled": a,
368
+ onClick: () => {
369
+ a || n(e.id);
370
+ },
371
+ className: "max-w-full whitespace-normal break-words",
372
+ children: [e.name || i.title, o && (o.count > 0 || o.truncated) ? /* @__PURE__ */ d("span", {
373
+ className: "tabular-nums",
374
+ children: [
375
+ " · ",
376
+ o.count,
377
+ o.truncated ? ` ${i.more}` : "",
378
+ " ",
379
+ i.unread
380
+ ]
381
+ }) : null]
382
+ }, e.id);
383
+ })
384
+ }), b = ({ scopeKey: e, dataScopeKey: t, channelId: n, messages: i, authors: a, labels: o = f.en, locale: s, loading: l = !1, onReport: p, onOpenMedia: y, canLoadOlder: b = !1, onLoadOlder: x, typing: S, typingLabels: C, ...w }) => {
385
+ let T = r(), E = e === t;
386
+ return /* @__PURE__ */ d("section", {
387
+ "aria-labelledby": T,
388
+ className: "min-w-0 space-y-4",
389
+ children: [
390
+ /* @__PURE__ */ u("h2", {
391
+ id: T,
392
+ className: "text-lg font-semibold",
393
+ children: o.title
394
+ }),
395
+ b && x ? /* @__PURE__ */ u(c, {
396
+ type: "button",
397
+ variant: "outline",
398
+ "aria-disabled": l || !E,
399
+ onClick: () => {
400
+ !l && E && x();
401
+ },
402
+ children: o.older
403
+ }) : null,
404
+ /* @__PURE__ */ u(m, {
405
+ messages: E ? i.filter((e) => e.channelId === n) : [],
406
+ authors: E ? a : [],
407
+ labels: o,
408
+ locale: s,
409
+ loading: l || !E,
410
+ onReport: E ? p : void 0,
411
+ onOpenMedia: E ? y : void 0
412
+ }, `${e}:${n}`),
413
+ /* @__PURE__ */ u(g, {
414
+ subjects: E && S?.channelId === n && S.status === "live" ? S.subjects : [],
415
+ authors: E ? a : [],
416
+ labels: C ?? (s?.startsWith("de") ? h.de : h.en),
417
+ locale: s
418
+ }),
419
+ /* @__PURE__ */ u(v, {
420
+ ...w,
421
+ value: E ? w.value : "",
422
+ labels: o,
423
+ mentionCandidates: E ? w.mentionCandidates : void 0,
424
+ mentionLabels: w.mentionLabels ?? (s?.startsWith("de") ? _.de : _.en),
425
+ onLoadMoreMentions: E ? w.onLoadMoreMentions : void 0,
426
+ disabled: w.disabled || !E
427
+ }, `${e}:${n}`)
428
+ ]
429
+ });
430
+ };
431
+ export { y as ChannelTabs, b as ChatPanel, v as Composer, m as MessageList, g as TypingIndicator, f as chatLabels, _ as chatMentionLabels, h as chatTypingLabels };
@@ -0,0 +1,91 @@
1
+ import { Schema as e } from "effect";
2
+ // src/contract.ts
3
+ var t = 200, n = 10, r = 2147483647, i = "_voltro_chat_channels", a = "_voltro_chat_members", o = "_voltro_chat_messages", s = "_voltro_chat_media", c = 50, l = [
4
+ "global",
5
+ "lobby",
6
+ "party",
7
+ "guild",
8
+ "match",
9
+ "dm",
10
+ "custom"
11
+ ], u = e.Number.pipe(e.check(e.isInt()), e.check(e.isGreaterThanOrEqualTo(0))), d = e.Record(e.String, e.Unknown), f = e.Struct({
12
+ id: e.String,
13
+ kind: e.Literals(l),
14
+ refId: e.String,
15
+ name: e.String,
16
+ slowModeMs: u,
17
+ retentionMs: e.NullOr(u),
18
+ maxMembers: e.Number.pipe(e.check(e.isInt()), e.check(e.isGreaterThan(0))),
19
+ frozen: e.Boolean,
20
+ createdAt: u,
21
+ metadata: d
22
+ }), p = e.Struct({
23
+ id: e.String,
24
+ channelId: e.String,
25
+ subjectId: e.String,
26
+ role: e.Literals([
27
+ "member",
28
+ "mod",
29
+ "owner"
30
+ ]),
31
+ mutedUntil: e.NullOr(u),
32
+ lastReadAt: u,
33
+ lastReadId: e.NullOr(e.String),
34
+ notify: e.Boolean,
35
+ metadata: d
36
+ }), m = e.Struct({
37
+ id: e.String,
38
+ channelId: e.String,
39
+ authorId: e.NullOr(e.String),
40
+ kind: e.Literals([
41
+ "text",
42
+ "emote",
43
+ "system",
44
+ "media"
45
+ ]),
46
+ body: e.String,
47
+ payload: d,
48
+ replyToId: e.NullOr(e.String),
49
+ editedAt: e.NullOr(u),
50
+ deletedAt: e.NullOr(u),
51
+ flagged: e.Boolean,
52
+ createdAt: u,
53
+ version: u,
54
+ metadata: d
55
+ }), h = e.Struct({
56
+ messages: e.Array(m),
57
+ before: e.NullOr(e.Struct({
58
+ at: u,
59
+ id: e.String
60
+ }))
61
+ }), g = e.Struct({
62
+ subjectId: e.String,
63
+ displayName: e.String,
64
+ avatar: e.NullOr(e.String)
65
+ }), _ = e.Struct({
66
+ authors: e.Array(g),
67
+ after: e.NullOr(e.String)
68
+ }), v = e.Struct({
69
+ channelId: e.String,
70
+ count: u,
71
+ truncated: e.Boolean
72
+ }), y = e.Struct({
73
+ message: m,
74
+ originalBody: e.String
75
+ });
76
+ e.Struct({
77
+ id: e.String,
78
+ contentType: e.String,
79
+ size: u,
80
+ width: e.NullOr(u),
81
+ height: e.NullOr(u)
82
+ });
83
+ var b = e.Struct({
84
+ url: e.String,
85
+ expiresAt: u
86
+ }), x = 5e3, S = e.Struct({
87
+ subjectId: e.NonEmptyString.pipe(e.check(e.isMaxLength(256))),
88
+ active: e.Boolean,
89
+ expiresAt: u
90
+ });
91
+ export { i as CHANNELS_TABLE, l as CHANNEL_KINDS, r as CHAT_MAX_MESSAGE_VERSION, c as CHAT_MEDIA_SWEEP_BATCH_SIZE, t as CHAT_RETENTION_BATCH_SIZE, n as CHAT_RETENTION_RUN_BATCHES, x as CHAT_TYPING_TTL_MS, g as ChatAuthorProjectionSchema, f as ChatChannelSchema, b as ChatMediaUrlSchema, p as ChatMemberSchema, _ as ChatMentionCandidatesSchema, h as ChatMessagePageSchema, m as ChatMessageSchema, y as ChatOriginalSchema, S as ChatTypingSchema, v as ChatUnreadSchema, s as MEDIA_TABLE, a as MEMBERS_TABLE, o as MESSAGES_TABLE };
@@ -0,0 +1,13 @@
1
+ import { Schema } from 'effect';
2
+ import { YieldableError } from 'effect/Cause';
3
+
4
+ export declare class ChatError extends ChatError_base {
5
+ }
6
+
7
+ export declare const chatError: (code: ChatError["code"]) => ChatError;
8
+
9
+ declare const ChatError_base: Schema.Class<ChatError, Schema.TaggedStruct<"ChatError", {
10
+ readonly code: Schema.Literals<readonly ["identity_required", "invalid_input", "not_found", "forbidden", "blocked", "sanctioned", "muted", "slow_mode", "rate_limited", "content_rejected", "conflict", "frozen", "member_limit", "unavailable", "export_limit"]>;
11
+ }>, YieldableError>;
12
+
13
+ export { }
package/dist/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ import { Schema as e } from "effect";
2
+ // src/errors.ts
3
+ var t = class extends e.TaggedError()("ChatError", { code: e.Literals([
4
+ "identity_required",
5
+ "invalid_input",
6
+ "not_found",
7
+ "forbidden",
8
+ "blocked",
9
+ "sanctioned",
10
+ "muted",
11
+ "slow_mode",
12
+ "rate_limited",
13
+ "content_rejected",
14
+ "conflict",
15
+ "frozen",
16
+ "member_limit",
17
+ "unavailable",
18
+ "export_limit"
19
+ ]) }) {}, n = (e) => new t({ code: e });
20
+ export { t as ChatError, n as chatError };
@@ -0,0 +1,18 @@
1
+ import { ChangeEvent } from '@voltro/database';
2
+ import { SubscribeContext } from '@voltro/runtime';
3
+
4
+ export declare const chatEventTenant: (event: ChangeEvent) => string;
5
+
6
+ declare interface ChatMentionDeliveryInput {
7
+ readonly tenantId: string;
8
+ readonly channelId: string;
9
+ readonly messageId: string;
10
+ readonly recipientId: string;
11
+ }
12
+
13
+ /** One committed-recipient interpretation for operator events and optional notification integration. */
14
+ export declare const committedChatMentions: (event: ChangeEvent) => ReadonlyArray<ChatMentionDeliveryInput>;
15
+
16
+ export declare const publishChatChange: (event: ChangeEvent, ctx: SubscribeContext) => Promise<void>;
17
+
18
+ export { }
@@ -0,0 +1,45 @@
1
+ import "./contract.js";
2
+ import { mentioned as e, messageChanged as t } from "./events.js";
3
+ // src/eventProducer.ts
4
+ var n = (e) => {
5
+ if (e.table !== "_voltro_chat_messages") throw Error("Chat events require the owned message table");
6
+ let t = (e.op === "delete" ? e.old : e.new)?.tenantId;
7
+ if (typeof t != "string" || !t.trim() || t.length > 256 || t.includes("\0") || t === "~" || e.op === "update" && e.old?.tenantId !== t) throw Error("Chat events require stable committed tenant evidence");
8
+ return t;
9
+ }, r = async (r, a) => {
10
+ n(r);
11
+ let o = r.op === "delete" ? r.old : r.new;
12
+ if (!o || typeof o.id != "string" || !o.id || o.id.length > 64 || typeof o.channelId != "string" || !o.channelId || !Number.isSafeInteger(o.version) || Number(o.version) < 0) throw Error("Chat events require committed identity/version evidence");
13
+ if (r.op === "update" && o.version === r.old?.version) return;
14
+ if (r.op === "update" && (!Number.isSafeInteger(r.old?.version) || Number(o.version) <= Number(r.old?.version))) throw Error("Chat event version must advance");
15
+ let s = i(r);
16
+ if (!a.publish) throw Error("Chat event publisher is unavailable");
17
+ await a.publish(t, { channelId: o.channelId }, {
18
+ messageId: o.id,
19
+ version: Number(o.version),
20
+ change: r.op === "delete" || o.deletedAt !== null ? "deleted" : r.op === "insert" ? "created" : "edited"
21
+ });
22
+ for (let t of s) await a.publish(e, {
23
+ channelId: t.channelId,
24
+ subjectId: t.recipientId
25
+ }, { messageId: t.messageId });
26
+ }, i = (e) => {
27
+ let t = n(e), r = e.new;
28
+ if (e.op !== "insert" || !r) return [];
29
+ if (typeof r.id != "string" || !r.id || r.id.length > 64 || typeof r.channelId != "string" || !r.channelId || r.channelId.length > 256) throw Error("Chat mentions require committed message/channel evidence");
30
+ let i = [];
31
+ if (e.op === "insert" && r.deletedAt === null && r.authorId !== null) {
32
+ let e = r.payload, t = e && typeof e == "object" && !Array.isArray(e) ? e.mentions : void 0;
33
+ if (t !== void 0) {
34
+ if (typeof r.authorId != "string" || !r.authorId.trim() || !Array.isArray(t) || t.length > 100 || t.some((e) => typeof e != "string" || !e.trim() || e.length > 256 || e.includes("\0") || e === "~")) throw Error("Chat mention events require bounded committed recipient evidence");
35
+ for (let e of new Set(t)) e !== r.authorId && i.push(e);
36
+ }
37
+ }
38
+ return i.map((e) => ({
39
+ tenantId: t,
40
+ channelId: r.channelId,
41
+ messageId: r.id,
42
+ recipientId: e
43
+ }));
44
+ };
45
+ export { n as chatEventTenant, i as committedChatMentions, r as publishChatChange };