@marktiderman/genesis-chat 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +163 -2
- package/dist/chunk-27UTR5FW.js +322 -0
- package/dist/chunk-27UTR5FW.js.map +1 -0
- package/dist/chunk-JQXZUEEL.js +525 -0
- package/dist/chunk-JQXZUEEL.js.map +1 -0
- package/dist/chunk-SAY24E6V.js +309 -0
- package/dist/chunk-SAY24E6V.js.map +1 -0
- package/dist/client-BtdALFCx.d.ts +349 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +3 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +137 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +311 -0
- package/dist/server.js +562 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +387 -0
- package/dist/web.js +2405 -0
- package/dist/web.js.map +1 -0
- package/migrations/0001_genesis_chat.sql +1836 -0
- package/migrations/tests/0001_genesis_chat.test.sql +254 -0
- package/package.json +144 -3
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @feat:chat
|
|
5
|
+
* The chat contract: what the genesis_chat schema (migration 0001) stores, in
|
|
6
|
+
* the app's words. Everything here is plain data, shared by web, native and
|
|
7
|
+
* the server.
|
|
8
|
+
*/
|
|
9
|
+
/** owner, admin or member of a conversation; observer when the kind's observer check lets you read it. */
|
|
10
|
+
type ChatRole = "owner" | "admin" | "member" | "observer";
|
|
11
|
+
/** Built in: image, file, voice, video, link. Apps add their own (form, lesson, worksheet…). */
|
|
12
|
+
type AttachmentKind = "image" | "file" | "voice" | "video" | "link" | (string & {});
|
|
13
|
+
type AttachmentProvider = "storage" | "mux" | "cloudflare" | "external" | "none";
|
|
14
|
+
interface ChatAttachment {
|
|
15
|
+
id: string;
|
|
16
|
+
messageId: string;
|
|
17
|
+
position: number;
|
|
18
|
+
kind: AttachmentKind;
|
|
19
|
+
/** The app's reference for its own kinds, e.g. `{ formId, versionHash }`. */
|
|
20
|
+
ref: Record<string, unknown>;
|
|
21
|
+
provider: AttachmentProvider;
|
|
22
|
+
/** The object in the genesis-chat bucket (`<conversation>/<uploader>/<name>.<ext>`). */
|
|
23
|
+
path: string | null;
|
|
24
|
+
providerId: string | null;
|
|
25
|
+
playbackId: string | null;
|
|
26
|
+
url: string | null;
|
|
27
|
+
status: "pending" | "ready" | "failed";
|
|
28
|
+
name: string | null;
|
|
29
|
+
mime: string | null;
|
|
30
|
+
bytes: number | null;
|
|
31
|
+
width: number | null;
|
|
32
|
+
height: number | null;
|
|
33
|
+
durationMs: number | null;
|
|
34
|
+
/** Peaks 0–100, at most 128, drawn before the audio loads. */
|
|
35
|
+
waveform: number[] | null;
|
|
36
|
+
posterPath: string | null;
|
|
37
|
+
thumbhash: string | null;
|
|
38
|
+
transcript: string | null;
|
|
39
|
+
transcriptStatus: "pending" | "ready" | "failed" | "off" | null;
|
|
40
|
+
}
|
|
41
|
+
interface ChatReaction {
|
|
42
|
+
emoji: string;
|
|
43
|
+
userId: string;
|
|
44
|
+
}
|
|
45
|
+
interface ChatMessage {
|
|
46
|
+
id: string;
|
|
47
|
+
conversationId: string;
|
|
48
|
+
seq: number;
|
|
49
|
+
clientId: string;
|
|
50
|
+
senderId: string | null;
|
|
51
|
+
senderRole: ChatRole | "system";
|
|
52
|
+
kind: "text" | "system";
|
|
53
|
+
/** Markdown. Empty for a deleted message. */
|
|
54
|
+
body: string;
|
|
55
|
+
mentions: string[];
|
|
56
|
+
replyToId: string | null;
|
|
57
|
+
threadRootId: string | null;
|
|
58
|
+
threadCount: number;
|
|
59
|
+
threadLastAt: string | null;
|
|
60
|
+
topic: string | null;
|
|
61
|
+
visibility: "everyone" | "observers";
|
|
62
|
+
silent: boolean;
|
|
63
|
+
meta: Record<string, unknown>;
|
|
64
|
+
createdAt: string;
|
|
65
|
+
editedAt: string | null;
|
|
66
|
+
deletedAt: string | null;
|
|
67
|
+
attachments: ChatAttachment[];
|
|
68
|
+
reactions: ChatReaction[];
|
|
69
|
+
}
|
|
70
|
+
interface ChatConversation {
|
|
71
|
+
id: string;
|
|
72
|
+
kind: string;
|
|
73
|
+
title: string | null;
|
|
74
|
+
subjectType: string | null;
|
|
75
|
+
subjectId: string | null;
|
|
76
|
+
settings: {
|
|
77
|
+
who_posts?: "members" | "admins";
|
|
78
|
+
} & Record<string, unknown>;
|
|
79
|
+
status: "open" | "pending" | "solved" | "closed" | null;
|
|
80
|
+
assigneeId: string | null;
|
|
81
|
+
lastSeq: number;
|
|
82
|
+
lastMessageAt: string | null;
|
|
83
|
+
createdAt: string;
|
|
84
|
+
archivedAt: string | null;
|
|
85
|
+
}
|
|
86
|
+
interface ChatMember {
|
|
87
|
+
conversationId: string;
|
|
88
|
+
userId: string;
|
|
89
|
+
role: ChatRole;
|
|
90
|
+
lastReadSeq: number;
|
|
91
|
+
joinedAt: string;
|
|
92
|
+
}
|
|
93
|
+
/** A kind's rules, as genesis_chat.kinds holds them. */
|
|
94
|
+
interface ChatKindRules {
|
|
95
|
+
kind: string;
|
|
96
|
+
label: string;
|
|
97
|
+
/** 2 for a direct conversation. */
|
|
98
|
+
maxMembers: number | null;
|
|
99
|
+
whoPosts: "members" | "admins";
|
|
100
|
+
membership: "app" | "admins";
|
|
101
|
+
threads: boolean;
|
|
102
|
+
observersPost: boolean;
|
|
103
|
+
}
|
|
104
|
+
/** The latest message, as the inbox previews it. */
|
|
105
|
+
interface ChatPreview {
|
|
106
|
+
id: string;
|
|
107
|
+
seq: number;
|
|
108
|
+
senderId: string | null;
|
|
109
|
+
kind: "text" | "system";
|
|
110
|
+
body: string;
|
|
111
|
+
deleted: boolean;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
attachment: AttachmentKind | null;
|
|
114
|
+
}
|
|
115
|
+
interface ChatInboxItem {
|
|
116
|
+
conversationId: string;
|
|
117
|
+
kind: string;
|
|
118
|
+
title: string | null;
|
|
119
|
+
subjectType: string | null;
|
|
120
|
+
subjectId: string | null;
|
|
121
|
+
status: ChatConversation["status"];
|
|
122
|
+
role: ChatRole;
|
|
123
|
+
/** The other person, in a two-person kind. */
|
|
124
|
+
otherUserId: string | null;
|
|
125
|
+
memberCount: number;
|
|
126
|
+
lastSeq: number;
|
|
127
|
+
lastReadSeq: number;
|
|
128
|
+
/** Counted to 99. */
|
|
129
|
+
unread: number;
|
|
130
|
+
mentioned: boolean;
|
|
131
|
+
muted: boolean;
|
|
132
|
+
pinned: boolean;
|
|
133
|
+
archived: boolean;
|
|
134
|
+
lastMessageAt: string | null;
|
|
135
|
+
lastMessage: ChatPreview | null;
|
|
136
|
+
}
|
|
137
|
+
/** What a sender attaches; files are uploaded first (ChatClient.upload). */
|
|
138
|
+
interface AttachmentInput {
|
|
139
|
+
kind: AttachmentKind;
|
|
140
|
+
path?: string;
|
|
141
|
+
provider?: AttachmentProvider;
|
|
142
|
+
providerId?: string;
|
|
143
|
+
url?: string;
|
|
144
|
+
ref?: Record<string, unknown>;
|
|
145
|
+
status?: "pending" | "ready";
|
|
146
|
+
name?: string;
|
|
147
|
+
mime?: string;
|
|
148
|
+
bytes?: number;
|
|
149
|
+
width?: number;
|
|
150
|
+
height?: number;
|
|
151
|
+
durationMs?: number;
|
|
152
|
+
waveform?: number[];
|
|
153
|
+
posterPath?: string;
|
|
154
|
+
thumbhash?: string;
|
|
155
|
+
}
|
|
156
|
+
interface SendInput {
|
|
157
|
+
conversationId: string;
|
|
158
|
+
/** Made once per message on the device, so a retry never posts twice. */
|
|
159
|
+
clientId: string;
|
|
160
|
+
body?: string;
|
|
161
|
+
attachments?: AttachmentInput[];
|
|
162
|
+
replyToId?: string | null;
|
|
163
|
+
threadRootId?: string | null;
|
|
164
|
+
topic?: string | null;
|
|
165
|
+
mentions?: string[];
|
|
166
|
+
visibility?: "everyone" | "observers";
|
|
167
|
+
silent?: boolean;
|
|
168
|
+
}
|
|
169
|
+
/** A message on its way: shown at once, swapped for the server's when it lands. */
|
|
170
|
+
interface PendingMessage {
|
|
171
|
+
clientId: string;
|
|
172
|
+
input: SendInput;
|
|
173
|
+
senderId: string;
|
|
174
|
+
createdAt: string;
|
|
175
|
+
status: "sending" | "failed";
|
|
176
|
+
error?: string;
|
|
177
|
+
/** Local previews for files still uploading (object URLs or file URIs). */
|
|
178
|
+
previews?: Record<number, string>;
|
|
179
|
+
/** What each file is, in order, so a pending bubble can draw it (a voice note's duration and waveform, a file's name). */
|
|
180
|
+
files?: PendingFile[];
|
|
181
|
+
}
|
|
182
|
+
interface PendingFile {
|
|
183
|
+
name: string;
|
|
184
|
+
contentType: string;
|
|
185
|
+
kind: AttachmentKind;
|
|
186
|
+
meta?: Partial<AttachmentInput>;
|
|
187
|
+
/** Not kept between launches (bytes with nowhere to save them): attach it again. */
|
|
188
|
+
lost?: boolean;
|
|
189
|
+
}
|
|
190
|
+
/** A live hint from Realtime: something changed; fetch to see what. */
|
|
191
|
+
type ChatSignal = {
|
|
192
|
+
type: "message";
|
|
193
|
+
conversationId: string;
|
|
194
|
+
id: string;
|
|
195
|
+
seq?: number;
|
|
196
|
+
op: "insert" | "update";
|
|
197
|
+
} | {
|
|
198
|
+
type: "reaction";
|
|
199
|
+
conversationId: string;
|
|
200
|
+
messageId: string;
|
|
201
|
+
} | {
|
|
202
|
+
type: "members";
|
|
203
|
+
conversationId: string;
|
|
204
|
+
} | {
|
|
205
|
+
type: "conversation";
|
|
206
|
+
conversationId: string;
|
|
207
|
+
} | {
|
|
208
|
+
type: "inbox";
|
|
209
|
+
conversationId: string;
|
|
210
|
+
seq?: number;
|
|
211
|
+
};
|
|
212
|
+
/** Ephemeral, never stored: someone typing or recording. */
|
|
213
|
+
interface ChatActivity {
|
|
214
|
+
userId: string;
|
|
215
|
+
activity: "typing" | "recording-voice" | "recording-video";
|
|
216
|
+
at: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @feat:chat
|
|
221
|
+
* The chat client: the app's own Supabase (migration 0001, the genesis_chat
|
|
222
|
+
* schema) under row level security. Reads are table selects; every write is a
|
|
223
|
+
* genesis_chat function. Live updates are hints on private Realtime topics,
|
|
224
|
+
* after which the client fetches again, so nothing is trusted from the wire.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/** Any supabase-js client; the app's generated Database types don't need to list genesis_chat. */
|
|
228
|
+
type ChatSupabase = SupabaseClient<any, any, any>;
|
|
229
|
+
declare const CHAT_SCHEMA = "genesis_chat";
|
|
230
|
+
declare const CHAT_BUCKET = "genesis-chat";
|
|
231
|
+
declare const conversationTopic: (conversationId: string) => string;
|
|
232
|
+
declare const userTopic: (userId: string) => string;
|
|
233
|
+
/** A refusal from the database, with its SQLSTATE (`42501` not allowed, `22023` bad input, `GS429` too many). */
|
|
234
|
+
declare class ChatError extends Error {
|
|
235
|
+
readonly code: string | undefined;
|
|
236
|
+
readonly operation: string;
|
|
237
|
+
constructor(message: string, code: string | undefined, operation: string);
|
|
238
|
+
get forbidden(): boolean;
|
|
239
|
+
get rateLimited(): boolean;
|
|
240
|
+
/** Worth trying again: the network, not the database, said no. */
|
|
241
|
+
get transient(): boolean;
|
|
242
|
+
}
|
|
243
|
+
type Row = Record<string, unknown>;
|
|
244
|
+
declare function toAttachment(r: Row): ChatAttachment;
|
|
245
|
+
declare function toMessage(r: Row): ChatMessage;
|
|
246
|
+
interface MessagePage {
|
|
247
|
+
messages: ChatMessage[];
|
|
248
|
+
/** More, older messages exist before the first one. */
|
|
249
|
+
hasEarlier: boolean;
|
|
250
|
+
}
|
|
251
|
+
interface ChatClientOptions {
|
|
252
|
+
/** Signed file links live this long (seconds). Default an hour. */
|
|
253
|
+
signedUrlSeconds?: number;
|
|
254
|
+
}
|
|
255
|
+
interface Subscription {
|
|
256
|
+
unsubscribe(): void;
|
|
257
|
+
}
|
|
258
|
+
interface ConversationChannel extends Subscription {
|
|
259
|
+
/** Tell the others you are typing or recording (throttle to once every 3 seconds). */
|
|
260
|
+
signal(activity: ChatActivity["activity"]): void;
|
|
261
|
+
}
|
|
262
|
+
declare function createChatClient(supabase: ChatSupabase, options?: ChatClientOptions): {
|
|
263
|
+
supabase: ChatSupabase;
|
|
264
|
+
inbox(opts?: {
|
|
265
|
+
kind?: string;
|
|
266
|
+
archived?: boolean;
|
|
267
|
+
limit?: number;
|
|
268
|
+
before?: string;
|
|
269
|
+
}): Promise<ChatInboxItem[]>;
|
|
270
|
+
observed(kind: string, opts?: {
|
|
271
|
+
status?: string;
|
|
272
|
+
limit?: number;
|
|
273
|
+
before?: string;
|
|
274
|
+
}): Promise<Row[]>;
|
|
275
|
+
unreadTotal(): Promise<number>;
|
|
276
|
+
conversation(id: string): Promise<ChatConversation | null>;
|
|
277
|
+
/** A kind's rules (how many members, who posts, threads), for what the UI offers. */
|
|
278
|
+
kind(kind: string): Promise<ChatKindRules | null>;
|
|
279
|
+
myRole(id: string): Promise<ChatRole | null>;
|
|
280
|
+
canPost(id: string): Promise<boolean>;
|
|
281
|
+
members(id: string): Promise<ChatMember[]>;
|
|
282
|
+
/**
|
|
283
|
+
* A page of messages, oldest first: the latest `limit` (default 50), or those
|
|
284
|
+
* before `beforeSeq`, or (to catch up after a reconnect) those after `afterSeq`.
|
|
285
|
+
* A thread's replies with `threadRootId`; one topic's with `topic`.
|
|
286
|
+
*/
|
|
287
|
+
messages(conversationId: string, opts?: {
|
|
288
|
+
beforeSeq?: number;
|
|
289
|
+
afterSeq?: number;
|
|
290
|
+
limit?: number;
|
|
291
|
+
threadRootId?: string;
|
|
292
|
+
topic?: string;
|
|
293
|
+
}): Promise<MessagePage>;
|
|
294
|
+
message(id: string): Promise<ChatMessage | null>;
|
|
295
|
+
startConversation(kind: string, memberIds: string[], opts?: {
|
|
296
|
+
title?: string;
|
|
297
|
+
subjectType?: string;
|
|
298
|
+
subjectId?: string;
|
|
299
|
+
}): Promise<string>;
|
|
300
|
+
/** Sends a message. Safe to retry with the same clientId. */
|
|
301
|
+
send(input: SendInput): Promise<ChatMessage>;
|
|
302
|
+
edit(messageId: string, body: string, mentions?: string[]): Promise<ChatMessage>;
|
|
303
|
+
remove(messageId: string): Promise<void>;
|
|
304
|
+
react(messageId: string, emoji: string, on?: boolean): Promise<void>;
|
|
305
|
+
markRead(conversationId: string, seq?: number): Promise<number>;
|
|
306
|
+
setMySettings(conversationId: string, s: {
|
|
307
|
+
mutedUntil?: string | null;
|
|
308
|
+
notify?: "all" | "mentions" | "none";
|
|
309
|
+
archived?: boolean;
|
|
310
|
+
pinned?: boolean;
|
|
311
|
+
}): Promise<void>;
|
|
312
|
+
updateConversation(conversationId: string, s: {
|
|
313
|
+
title?: string;
|
|
314
|
+
settings?: Record<string, unknown>;
|
|
315
|
+
}): Promise<void>;
|
|
316
|
+
addMembers(conversationId: string, userIds: string[]): Promise<void>;
|
|
317
|
+
removeMember(conversationId: string, userId?: string): Promise<void>;
|
|
318
|
+
setStatus(conversationId: string, status: ChatConversation["status"], assigneeId?: string | null): Promise<void>;
|
|
319
|
+
pin(messageId: string, on?: boolean): Promise<void>;
|
|
320
|
+
pins(conversationId: string): Promise<string[]>;
|
|
321
|
+
save(messageId: string, on?: boolean, note?: string): Promise<void>;
|
|
322
|
+
report(messageId: string, reason: "spam" | "harassment" | "safety" | "other", note?: string): Promise<string>;
|
|
323
|
+
/**
|
|
324
|
+
* Uploads a file into the conversation's folder for the signed-in person and
|
|
325
|
+
* returns its path, ready to attach. Resumable for big files is the native
|
|
326
|
+
* renderer's job (Supabase TUS); this is the plain upload.
|
|
327
|
+
*/
|
|
328
|
+
upload(conversationId: string, userId: string, file: Blob | ArrayBuffer, opts: {
|
|
329
|
+
name: string;
|
|
330
|
+
contentType: string;
|
|
331
|
+
id?: string;
|
|
332
|
+
}): Promise<string>;
|
|
333
|
+
/** Signed links for files, cached until shortly before they expire. */
|
|
334
|
+
fileUrls(paths: string[]): Promise<Record<string, string>>;
|
|
335
|
+
/** Your inbox's live hints (a new message anywhere you are, a membership change). */
|
|
336
|
+
subscribeInbox(userId: string, onSignal: (s: ChatSignal) => void): Subscription;
|
|
337
|
+
/**
|
|
338
|
+
* A conversation's live hints, plus who is typing or recording. `onReconnect`
|
|
339
|
+
* runs whenever the channel (re)joins, the moment to fetch what was missed.
|
|
340
|
+
*/
|
|
341
|
+
subscribeConversation(conversationId: string, me: string, handlers: {
|
|
342
|
+
onSignal: (s: ChatSignal) => void;
|
|
343
|
+
onActivity?: (a: ChatActivity) => void;
|
|
344
|
+
onReconnect?: () => void;
|
|
345
|
+
}): ConversationChannel;
|
|
346
|
+
};
|
|
347
|
+
type ChatClient = ReturnType<typeof createChatClient>;
|
|
348
|
+
|
|
349
|
+
export { type AttachmentKind as A, type ChatClient as C, type MessagePage as M, type PendingMessage as P, type SendInput as S, type ChatConversation as a, type ChatMember as b, type ChatRole as c, type ChatKindRules as d, type ChatMessage as e, type ChatActivity as f, type ChatSignal as g, type ChatInboxItem as h, type ChatSupabase as i, type ChatAttachment as j, type AttachmentInput as k, type PendingFile as l, type AttachmentProvider as m, CHAT_BUCKET as n, CHAT_SCHEMA as o, type ChatClientOptions as p, ChatError as q, type ChatPreview as r, type ChatReaction as s, type ConversationChannel as t, type Subscription as u, conversationTopic as v, createChatClient as w, toAttachment as x, toMessage as y, userTopic as z };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { A as AttachmentKind, e as ChatMessage, P as PendingMessage, f as ChatActivity, k as AttachmentInput, C as ChatClient, l as PendingFile, S as SendInput } from './client-BtdALFCx.js';
|
|
2
|
+
export { m as AttachmentProvider, n as CHAT_BUCKET, o as CHAT_SCHEMA, j as ChatAttachment, p as ChatClientOptions, a as ChatConversation, q as ChatError, h as ChatInboxItem, d as ChatKindRules, b as ChatMember, r as ChatPreview, s as ChatReaction, c as ChatRole, g as ChatSignal, i as ChatSupabase, t as ConversationChannel, M as MessagePage, u as Subscription, v as conversationTopic, w as createChatClient, x as toAttachment, y as toMessage, z as userTopic } from './client-BtdALFCx.js';
|
|
3
|
+
import '@supabase/supabase-js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @feat:chat
|
|
7
|
+
* Media rules shared by every platform: which files may be sent (the same list
|
|
8
|
+
* migration 0001 checks), what kind an upload becomes, the default limits, and
|
|
9
|
+
* the waveform a voice note carries so its bubble draws before the audio loads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A random UUID (v4): `crypto.randomUUID` where the platform has it (some React Native
|
|
14
|
+
* runtimes don't), else `crypto.getRandomValues`, else Math.random as a last resort. Ids
|
|
15
|
+
* only need to be unique per sender: the server checks (sender, client_id).
|
|
16
|
+
*/
|
|
17
|
+
declare function randomId(): string;
|
|
18
|
+
/** Mirrors genesis_chat.allowed_extension. Never SVG or HTML, which a browser runs as a page. */
|
|
19
|
+
declare const ALLOWED_EXTENSIONS: readonly ["png", "jpg", "jpeg", "webp", "gif", "heic", "heif", "m4a", "aac", "mp3", "ogg", "oga", "opus", "weba", "webm", "mp4", "mov", "pdf", "txt", "csv", "docx", "xlsx", "pptx"];
|
|
20
|
+
/** The extension a file is stored under: from its content type, else its name. */
|
|
21
|
+
declare function fileExtension(name: string, contentType?: string): string;
|
|
22
|
+
declare function isAllowedExtension(ext: string): boolean;
|
|
23
|
+
/** What an upload becomes in a message. */
|
|
24
|
+
declare function attachmentKindFor(contentType: string, opts?: {
|
|
25
|
+
recordedVoice?: boolean;
|
|
26
|
+
}): AttachmentKind;
|
|
27
|
+
/** Default limits (decision 5): an app may lower them, not raise them past the bucket's 100 MB. */
|
|
28
|
+
declare const CHAT_LIMITS: {
|
|
29
|
+
/** Voice notes and videos, for members. */
|
|
30
|
+
readonly recordingMs: number;
|
|
31
|
+
/** For owners, admins and observers (coaches, staff). */
|
|
32
|
+
readonly staffRecordingMs: number;
|
|
33
|
+
readonly fileBytes: number;
|
|
34
|
+
readonly videoBytes: number;
|
|
35
|
+
readonly attachmentsPerMessage: 20;
|
|
36
|
+
readonly bodyChars: 10000;
|
|
37
|
+
};
|
|
38
|
+
/** The longest recording this role may make. */
|
|
39
|
+
declare function recordingLimitMs(role: string | null | undefined): number;
|
|
40
|
+
/**
|
|
41
|
+
* Peaks for a waveform: `count` values 0–100 from raw samples (-1…1), the loudest
|
|
42
|
+
* in each bucket, scaled so the loudest bucket is 100. Quiet recordings still show
|
|
43
|
+
* a shape; silence shows a flat line.
|
|
44
|
+
*/
|
|
45
|
+
declare function waveformPeaks(samples: ArrayLike<number>, count?: number): number[];
|
|
46
|
+
/**
|
|
47
|
+
* Peaks from a level meter read while recording (expo-audio's `metering`, in dB,
|
|
48
|
+
* about -160…0): resampled to `count` values 0–100.
|
|
49
|
+
*/
|
|
50
|
+
declare function waveformFromMetering(levelsDb: number[], count?: number): number[];
|
|
51
|
+
/** 0:42, 1:05:09. */
|
|
52
|
+
declare function formatDuration(ms: number | null | undefined): string;
|
|
53
|
+
/** Playback speeds offered for voice and video (free for everyone, decision 5). */
|
|
54
|
+
declare const PLAYBACK_RATES: readonly [1, 1.5, 2];
|
|
55
|
+
declare function nextPlaybackRate(rate: number): number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @feat:chat
|
|
59
|
+
* Pure thread logic, the same on every platform: merging the server's messages
|
|
60
|
+
* with ones still on their way, grouping a run of messages from one sender,
|
|
61
|
+
* day dividers, the unread line, reactions by emoji, and the typing line.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
type ThreadItem = {
|
|
65
|
+
type: "message";
|
|
66
|
+
message: ChatMessage;
|
|
67
|
+
pending?: undefined;
|
|
68
|
+
} | {
|
|
69
|
+
type: "pending";
|
|
70
|
+
pending: PendingMessage;
|
|
71
|
+
message?: undefined;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The server's messages (by seq), then what is still sending, oldest first. A
|
|
75
|
+
* pending message the server already has (same clientId) is dropped, so a send
|
|
76
|
+
* that landed never shows twice.
|
|
77
|
+
*/
|
|
78
|
+
declare function mergeThread(server: ChatMessage[], pending: PendingMessage[]): ThreadItem[];
|
|
79
|
+
/** Adds or replaces messages in a page, keeping it sorted and free of duplicates. */
|
|
80
|
+
declare function upsertMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
|
|
81
|
+
declare function itemSender(item: ThreadItem): string | null;
|
|
82
|
+
declare function itemTime(item: ThreadItem): string;
|
|
83
|
+
/** Runs of one sender within five minutes share a header (name, avatar). */
|
|
84
|
+
declare function startsGroup(items: ThreadItem[], index: number, gapMs?: number): boolean;
|
|
85
|
+
declare function sameDay(a: string, b: string): boolean;
|
|
86
|
+
/** A day divider goes before this item. */
|
|
87
|
+
declare function startsDay(items: ThreadItem[], index: number): boolean;
|
|
88
|
+
/** "Today", "Yesterday", "Monday", or "12 Mar 2026". */
|
|
89
|
+
declare function dayLabel(iso: string, now?: Date, locale?: string): string;
|
|
90
|
+
/** The first message after what you had read, from someone else: where "New" goes. */
|
|
91
|
+
declare function firstUnreadId(messages: ChatMessage[], lastReadSeq: number, me: string): string | null;
|
|
92
|
+
interface ReactionSummary {
|
|
93
|
+
emoji: string;
|
|
94
|
+
count: number;
|
|
95
|
+
mine: boolean;
|
|
96
|
+
userIds: string[];
|
|
97
|
+
}
|
|
98
|
+
/** Reactions by emoji, most first, then in first-used order. */
|
|
99
|
+
declare function summarizeReactions(message: ChatMessage, me: string): ReactionSummary[];
|
|
100
|
+
/** Who is doing what now: activity older than six seconds has stopped. */
|
|
101
|
+
declare function currentActivity(activity: ChatActivity[], now?: number, staleMs?: number): ChatActivity[];
|
|
102
|
+
/** "Sam is typing…", "Sam and Lee are typing…", "Sam is recording a voice message…". */
|
|
103
|
+
declare function activityLine(activity: ChatActivity[], nameOf: (userId: string) => string): string | null;
|
|
104
|
+
/** Sends activity at most once every `everyMs`. */
|
|
105
|
+
declare function throttle(fn: () => void, everyMs?: number): () => void;
|
|
106
|
+
/** The ids @mentioned in markdown written as `@[Name](user:<uuid>)`. */
|
|
107
|
+
declare function parseMentions(body: string): string[];
|
|
108
|
+
/** A one-line preview: text, or what was attached. */
|
|
109
|
+
declare function previewText(body: string, attachment: string | null | undefined, deleted?: boolean): string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @feat:chat
|
|
113
|
+
* The send queue: a message shows at once as pending, its files upload, then it
|
|
114
|
+
* is sent with the clientId made when it was written, so a retry (after a lost
|
|
115
|
+
* connection, a closed app) never posts twice. Messages to one conversation go
|
|
116
|
+
* in the order they were written. Storage is the platform's (memory by default;
|
|
117
|
+
* localStorage on web, MMKV or SQLite on native), behind OutboxStore.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/** A file still on the device, uploaded when its message is sent. */
|
|
121
|
+
interface LocalFile {
|
|
122
|
+
/** The bytes, or (native) a file URI the platform reads in `readFile`. */
|
|
123
|
+
data: Blob | ArrayBuffer | string;
|
|
124
|
+
name: string;
|
|
125
|
+
contentType: string;
|
|
126
|
+
/** A preview to show while it uploads (an object URL, a file URI). */
|
|
127
|
+
previewUrl?: string;
|
|
128
|
+
/** Known up front: a voice note's duration and waveform, a photo's size. */
|
|
129
|
+
meta?: Partial<Omit<AttachmentInput, "path" | "kind">> & {
|
|
130
|
+
kind?: AttachmentInput["kind"];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
interface QueuedMessage extends Omit<PendingMessage, "files"> {
|
|
134
|
+
files: LocalFile[];
|
|
135
|
+
/** What each file is, kept with the saved copy (PendingMessage.files). */
|
|
136
|
+
pendingFiles?: PendingFile[];
|
|
137
|
+
/** Files lost on a relaunch (bytes that weren't persisted). */
|
|
138
|
+
lostFiles?: number;
|
|
139
|
+
attempts: number;
|
|
140
|
+
}
|
|
141
|
+
interface OutboxStore {
|
|
142
|
+
load(): Promise<QueuedMessage[]>;
|
|
143
|
+
save(queue: QueuedMessage[]): Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
declare const memoryOutboxStore: () => OutboxStore;
|
|
146
|
+
interface OutboxOptions {
|
|
147
|
+
client: ChatClient;
|
|
148
|
+
userId: string;
|
|
149
|
+
store?: OutboxStore;
|
|
150
|
+
/** Reads a native file URI into bytes for upload. */
|
|
151
|
+
readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
|
|
152
|
+
/** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. */
|
|
153
|
+
upload?: (conversationId: string, file: LocalFile) => Promise<string>;
|
|
154
|
+
/** Called with every change, for the UI. */
|
|
155
|
+
onChange?: (queue: QueuedMessage[]) => void;
|
|
156
|
+
/** Called when a message lands. */
|
|
157
|
+
onSent?: (message: ChatMessage, clientId: string) => void;
|
|
158
|
+
/** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */
|
|
159
|
+
newId?: () => string;
|
|
160
|
+
/**
|
|
161
|
+
* Keeps a file held as bytes (a pasted image, a recording) between launches: write it
|
|
162
|
+
* somewhere and return a URI `readFile` can read (native: the app's cache directory).
|
|
163
|
+
* Without it, such a file isn't saved, and after a relaunch its message asks for it again.
|
|
164
|
+
*/
|
|
165
|
+
persistFile?: (file: LocalFile) => Promise<string>;
|
|
166
|
+
}
|
|
167
|
+
declare function createOutbox(opts: OutboxOptions): {
|
|
168
|
+
/** Queues a message; it shows at once and sends in order. Returns its clientId. */
|
|
169
|
+
send(input: Omit<SendInput, "clientId"> & {
|
|
170
|
+
clientId?: string;
|
|
171
|
+
}, files?: LocalFile[]): Promise<string>;
|
|
172
|
+
retry(clientId: string): Promise<void>;
|
|
173
|
+
/** Gives a message whose files were lost on a relaunch its files again, and sends it. */
|
|
174
|
+
reattach(clientId: string, files: LocalFile[]): Promise<void>;
|
|
175
|
+
/** Retries every failed message (after the connection comes back). */
|
|
176
|
+
retryAll(): Promise<void>;
|
|
177
|
+
discard(clientId: string): Promise<void>;
|
|
178
|
+
pending(conversationId?: string): PendingMessage[];
|
|
179
|
+
/** A new client id, made the way this outbox makes them. */
|
|
180
|
+
newId: typeof randomId;
|
|
181
|
+
load: () => Promise<void>;
|
|
182
|
+
};
|
|
183
|
+
type Outbox = ReturnType<typeof createOutbox>;
|
|
184
|
+
|
|
185
|
+
export { ALLOWED_EXTENSIONS, AttachmentInput, AttachmentKind, CHAT_LIMITS, ChatActivity, ChatClient, ChatMessage, type LocalFile, type Outbox, type OutboxOptions, type OutboxStore, PLAYBACK_RATES, PendingFile, PendingMessage, type QueuedMessage, type ReactionSummary, SendInput, type ThreadItem, activityLine, attachmentKindFor, createOutbox, currentActivity, dayLabel, fileExtension, firstUnreadId, formatDuration, isAllowedExtension, itemSender, itemTime, memoryOutboxStore, mergeThread, nextPlaybackRate, parseMentions, previewText, randomId, recordingLimitMs, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages, waveformFromMetering, waveformPeaks };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages } from './chunk-SAY24E6V.js';
|
|
2
|
+
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 } from './chunk-JQXZUEEL.js';
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/native.d.ts
ADDED
package/dist/native.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"native.js"}
|