@noverachat/sdk-react 0.3.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/dist/index.cjs ADDED
@@ -0,0 +1,1192 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ MemberListStore: () => MemberListStore,
25
+ NoveraChatProvider: () => NoveraChatProvider,
26
+ RoomFilesStore: () => RoomFilesStore,
27
+ RoomListStore: () => RoomListStore,
28
+ RoomSettingsStore: () => RoomSettingsStore,
29
+ RoomStore: () => RoomStore,
30
+ chatMessageFromHistory: () => chatMessageFromHistory,
31
+ chatMessageFromLive: () => chatMessageFromLive,
32
+ useMemberList: () => useMemberList,
33
+ useMessages: () => useMessages,
34
+ useNoveraChat: () => useNoveraChat,
35
+ useRoom: () => useRoom,
36
+ useRoomFiles: () => useRoomFiles,
37
+ useRoomList: () => useRoomList,
38
+ useRoomSettings: () => useRoomSettings,
39
+ useTyping: () => useTyping,
40
+ useUnread: () => useUnread
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+ __reExport(index_exports, require("@noverachat/sdk-web"), module.exports);
44
+
45
+ // src/chat-message.ts
46
+ function fileInlineFromWire(w) {
47
+ return {
48
+ id: w.id,
49
+ kind: w.kind,
50
+ mime: w.mime,
51
+ size: w.size,
52
+ name: w.name ?? null,
53
+ width: w.width ?? null,
54
+ height: w.height ?? null,
55
+ durationSec: w.duration_sec ?? null,
56
+ thumbnailStatus: w.thumbnail_status,
57
+ forwardBlocked: Boolean(w.forward_blocked)
58
+ };
59
+ }
60
+ function chatMessageFromHistory(m) {
61
+ return {
62
+ id: m.messageId,
63
+ senderId: m.senderId ?? null,
64
+ content: m.content ?? null,
65
+ createdAtMs: m.createdAtMs,
66
+ status: "sent",
67
+ messageType: m.messageType,
68
+ sender: m.sender ?? null,
69
+ customType: m.customType ?? null,
70
+ fileId: m.fileId ?? null,
71
+ file: m.file ?? null,
72
+ files: m.files ?? null,
73
+ replyToId: m.replyToId ?? null,
74
+ reactions: m.reactions ?? [],
75
+ customMeta: m.customMeta ?? null,
76
+ editedCount: m.editedCount ?? 0,
77
+ uploadProgress: null,
78
+ tempId: null,
79
+ isDeleted: m.deletedAt != null
80
+ };
81
+ }
82
+ function chatMessageFromLive(f) {
83
+ const data = f.data ?? null;
84
+ const customMeta = data && typeof data === "object" && "_customMeta" in data ? data._customMeta : null;
85
+ return {
86
+ id: f.message_id,
87
+ senderId: f.sender_id,
88
+ content: f.content ?? null,
89
+ createdAtMs: f.created_at,
90
+ status: "sent",
91
+ messageType: f.message_type,
92
+ sender: null,
93
+ customType: f.custom_type ?? null,
94
+ fileId: f.file_id ?? null,
95
+ file: f.file ? fileInlineFromWire(f.file) : null,
96
+ files: f.files ? f.files.map(fileInlineFromWire) : null,
97
+ replyToId: f.reply_to_id ?? null,
98
+ reactions: [],
99
+ customMeta,
100
+ editedCount: 0,
101
+ uploadProgress: null,
102
+ tempId: null,
103
+ isDeleted: false
104
+ };
105
+ }
106
+ function pendingChatMessage(p) {
107
+ return {
108
+ id: p.tempId,
109
+ senderId: null,
110
+ content: p.content,
111
+ createdAtMs: Date.now(),
112
+ status: "sending",
113
+ messageType: "TEXT",
114
+ sender: null,
115
+ customType: p.customType ?? null,
116
+ fileId: null,
117
+ file: null,
118
+ files: null,
119
+ replyToId: p.replyToId ?? null,
120
+ reactions: [],
121
+ customMeta: p.customMeta ?? null,
122
+ editedCount: 0,
123
+ uploadProgress: null,
124
+ tempId: p.tempId,
125
+ isDeleted: false
126
+ };
127
+ }
128
+ function pendingFileChatMessage(p) {
129
+ const mime = p.mime ?? "application/octet-stream";
130
+ return {
131
+ id: p.tempId,
132
+ senderId: null,
133
+ content: null,
134
+ createdAtMs: Date.now(),
135
+ status: "sending",
136
+ messageType: "FILE",
137
+ sender: null,
138
+ customType: null,
139
+ fileId: null,
140
+ file: {
141
+ id: "",
142
+ kind: mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : "file",
143
+ mime,
144
+ size: p.size ?? 0,
145
+ name: p.name,
146
+ thumbnailStatus: "pending",
147
+ forwardBlocked: false
148
+ },
149
+ files: null,
150
+ replyToId: null,
151
+ reactions: [],
152
+ customMeta: null,
153
+ editedCount: 0,
154
+ uploadProgress: 0,
155
+ tempId: p.tempId,
156
+ isDeleted: false
157
+ };
158
+ }
159
+
160
+ // src/context.tsx
161
+ var import_react = require("react");
162
+ var import_sdk_web = require("@noverachat/sdk-web");
163
+ var import_jsx_runtime = require("react/jsx-runtime");
164
+ var NoveraChatContext = (0, import_react.createContext)(null);
165
+ function NoveraChatProvider(props) {
166
+ const [chat] = (0, import_react.useState)(() => new import_sdk_web.NoveraChat(props.options));
167
+ (0, import_react.useEffect)(() => {
168
+ void chat.connect();
169
+ return () => chat.disconnect();
170
+ }, [chat]);
171
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoveraChatContext.Provider, { value: chat, children: props.children });
172
+ }
173
+ function useNoveraChat() {
174
+ const chat = (0, import_react.useContext)(NoveraChatContext);
175
+ if (!chat) {
176
+ throw new Error(
177
+ "useNoveraChat() called with no NoveraChatProvider in the tree."
178
+ );
179
+ }
180
+ return chat;
181
+ }
182
+
183
+ // src/member-list-store.ts
184
+ var MemberListStore = class {
185
+ chat;
186
+ roomId;
187
+ listeners = /* @__PURE__ */ new Set();
188
+ members = [];
189
+ unsubs = [];
190
+ reloadTimer = null;
191
+ snapshot = {
192
+ members: [],
193
+ operators: [],
194
+ activeMemberIds: []
195
+ };
196
+ attached = false;
197
+ constructor(chat, roomId) {
198
+ this.chat = chat;
199
+ this.roomId = roomId;
200
+ }
201
+ attach() {
202
+ if (this.attached) return;
203
+ this.attached = true;
204
+ this.unsubs.push(
205
+ this.chat.on("roomEvent", (e) => {
206
+ if (e.room_id === this.roomId) this.scheduleReload();
207
+ }),
208
+ this.chat.on("presence", (f) => this.onPresence(f))
209
+ );
210
+ }
211
+ dispose() {
212
+ if (!this.attached) return;
213
+ this.attached = false;
214
+ if (this.reloadTimer) clearTimeout(this.reloadTimer);
215
+ this.reloadTimer = null;
216
+ for (const u of this.unsubs.splice(0)) u();
217
+ }
218
+ // ── external-store contract ─────────────────────────────────────
219
+ subscribe = (fn) => {
220
+ this.listeners.add(fn);
221
+ return () => this.listeners.delete(fn);
222
+ };
223
+ getSnapshot = () => this.snapshot;
224
+ emit() {
225
+ this.snapshot = {
226
+ members: [...this.members],
227
+ operators: this.members.filter((m) => m.role === "OPERATOR"),
228
+ activeMemberIds: this.members.filter((m) => m.role !== "KICKED").map((m) => m.userId)
229
+ };
230
+ for (const fn of this.listeners) fn();
231
+ }
232
+ /** Whether `userId` is an OPERATOR of this room. */
233
+ isOperator(userId) {
234
+ return this.members.some(
235
+ (m) => m.userId === userId && m.role === "OPERATOR"
236
+ );
237
+ }
238
+ /** Fetch the member list. Also called automatically on membership
239
+ * events. */
240
+ async load() {
241
+ const list = await this.chat.room(this.roomId).listMembers();
242
+ if (!this.attached) return;
243
+ this.members = list;
244
+ this.emit();
245
+ }
246
+ // ── internals ───────────────────────────────────────────────────
247
+ onPresence(f) {
248
+ const i = this.members.findIndex((m) => m.userId === f.user_id);
249
+ if (i === -1) return;
250
+ this.members[i] = { ...this.members[i], isOnline: f.online };
251
+ this.emit();
252
+ }
253
+ scheduleReload() {
254
+ if (this.reloadTimer) clearTimeout(this.reloadTimer);
255
+ this.reloadTimer = setTimeout(() => {
256
+ this.reloadTimer = null;
257
+ if (this.attached) void this.load();
258
+ }, 300);
259
+ }
260
+ };
261
+
262
+ // src/room-files-store.ts
263
+ var RoomFilesStore = class {
264
+ chat;
265
+ roomId;
266
+ /** Page size for both `refresh` and `loadMore`. */
267
+ pageSize;
268
+ listeners = /* @__PURE__ */ new Set();
269
+ items = [];
270
+ cursor = null;
271
+ hasMore = false;
272
+ loading = false;
273
+ attached = false;
274
+ snapshot = {
275
+ items: [],
276
+ media: [],
277
+ hasMore: false,
278
+ isLoading: false
279
+ };
280
+ constructor(chat, roomId, pageSize = 50) {
281
+ this.chat = chat;
282
+ this.roomId = roomId;
283
+ this.pageSize = pageSize;
284
+ }
285
+ /** No realtime subscriptions — kept for lifecycle symmetry with the
286
+ * other stores (guards in-flight results after unmount). */
287
+ attach() {
288
+ this.attached = true;
289
+ }
290
+ dispose() {
291
+ this.attached = false;
292
+ }
293
+ // ── external-store contract ─────────────────────────────────────
294
+ subscribe = (fn) => {
295
+ this.listeners.add(fn);
296
+ return () => this.listeners.delete(fn);
297
+ };
298
+ getSnapshot = () => this.snapshot;
299
+ emit() {
300
+ this.snapshot = {
301
+ items: [...this.items],
302
+ media: this.items.filter(
303
+ (it) => it.file.kind === "image" || it.file.kind === "video"
304
+ ),
305
+ hasMore: this.hasMore,
306
+ isLoading: this.loading
307
+ };
308
+ for (const fn of this.listeners) fn();
309
+ }
310
+ // ── actions ─────────────────────────────────────────────────────
311
+ /** Reload from the newest page (clears what was loaded). */
312
+ async refresh() {
313
+ if (this.loading) return;
314
+ this.loading = true;
315
+ this.emit();
316
+ try {
317
+ const page = await this.chat.room(this.roomId).listFiles({ limit: this.pageSize });
318
+ if (!this.attached) return;
319
+ this.items = page.items;
320
+ this.cursor = page.nextCursor;
321
+ this.hasMore = page.hasMore;
322
+ } finally {
323
+ this.loading = false;
324
+ if (this.attached) this.emit();
325
+ }
326
+ }
327
+ /** Fetch the next older page and append it. No-op while loading or when
328
+ * `hasMore` is false. Returns the number of entries added. */
329
+ async loadMore() {
330
+ const cursor = this.cursor;
331
+ if (this.loading || !this.hasMore || cursor == null) return 0;
332
+ this.loading = true;
333
+ this.emit();
334
+ try {
335
+ const page = await this.chat.room(this.roomId).listFiles({ limit: this.pageSize, before: cursor });
336
+ if (!this.attached) return 0;
337
+ this.items = [...this.items, ...page.items];
338
+ this.cursor = page.nextCursor;
339
+ this.hasMore = page.hasMore;
340
+ return page.items.length;
341
+ } finally {
342
+ this.loading = false;
343
+ if (this.attached) this.emit();
344
+ }
345
+ }
346
+ };
347
+
348
+ // src/room-list-store.ts
349
+ function itemFromUnread(r) {
350
+ const members = r.membersPreview ?? [];
351
+ return {
352
+ roomId: r.roomId,
353
+ name: r.name,
354
+ roomType: r.roomType ?? null,
355
+ unreadCount: r.unreadCount,
356
+ lastMessageId: r.lastMessageId ?? null,
357
+ lastMessagePreview: r.lastMessagePreview ?? null,
358
+ memberCount: r.memberCount ?? 0,
359
+ members,
360
+ lastMessageAtMs: null,
361
+ anyMemberOnline: members.some((m) => m.isOnline === true)
362
+ };
363
+ }
364
+ var RoomListStore = class {
365
+ chat;
366
+ opts;
367
+ listeners = /* @__PURE__ */ new Set();
368
+ rooms = [];
369
+ index = /* @__PURE__ */ new Map();
370
+ // roomId → rooms position
371
+ roomSubs = [];
372
+ clientSub = null;
373
+ reloadTimer = null;
374
+ snapshot = { rooms: [], totalUnread: 0 };
375
+ attached = false;
376
+ constructor(chat, opts = {}) {
377
+ this.chat = chat;
378
+ this.opts = opts;
379
+ }
380
+ /** Subscribe to client-level room events. Idempotent. */
381
+ attach() {
382
+ if (this.attached) return;
383
+ this.attached = true;
384
+ this.clientSub = this.chat.on("roomEvent", () => this.scheduleReload());
385
+ }
386
+ /** Unsubscribe everything (list state is kept for a later `attach`). */
387
+ dispose() {
388
+ if (!this.attached) return;
389
+ this.attached = false;
390
+ if (this.reloadTimer) clearTimeout(this.reloadTimer);
391
+ this.reloadTimer = null;
392
+ for (const u of this.roomSubs.splice(0)) u();
393
+ this.clientSub?.();
394
+ this.clientSub = null;
395
+ }
396
+ // ── external-store contract ─────────────────────────────────────
397
+ subscribe = (fn) => {
398
+ this.listeners.add(fn);
399
+ return () => this.listeners.delete(fn);
400
+ };
401
+ getSnapshot = () => this.snapshot;
402
+ emit() {
403
+ this.snapshot = {
404
+ rooms: [...this.rooms],
405
+ totalUnread: this.rooms.reduce((n, r) => n + r.unreadCount, 0)
406
+ };
407
+ for (const fn of this.listeners) fn();
408
+ }
409
+ // ── actions ─────────────────────────────────────────────────────
410
+ /** Reload the room list and re-hook the realtime subscriptions. */
411
+ async load() {
412
+ const summary = await this.chat.unreadSummary();
413
+ if (!this.attached) return;
414
+ this.rooms = summary.rooms.map(itemFromUnread);
415
+ this.reindex();
416
+ this.resubscribe();
417
+ this.emit();
418
+ }
419
+ /** Hide a room (카톡 "숨기기") — optimistically remove it from the list
420
+ * and send `hide` to the server. On failure the next `load` brings it
421
+ * back; new activity may re-surface it per server policy (arrives via
422
+ * roomEvent → reload). */
423
+ async hideRoom(roomId) {
424
+ const i = this.index.get(roomId);
425
+ if (i !== void 0) {
426
+ this.rooms.splice(i, 1);
427
+ this.reindex();
428
+ this.emit();
429
+ }
430
+ await this.chat.room(roomId).hide();
431
+ }
432
+ /** Undo `hideRoom` — send `unhide` and reload the list. */
433
+ async unhideRoom(roomId) {
434
+ await this.chat.room(roomId).unhide();
435
+ await this.load();
436
+ }
437
+ /** Mark a room read — drop its badge to 0 and advance the client's
438
+ * last-seen watermark. */
439
+ markRead(roomId) {
440
+ const i = this.index.get(roomId);
441
+ if (i === void 0) return;
442
+ const item = this.rooms[i];
443
+ if (item.lastMessageId) this.chat.setLastSeen(roomId, item.lastMessageId);
444
+ if (item.unreadCount !== 0) {
445
+ this.rooms[i] = { ...item, unreadCount: 0 };
446
+ this.emit();
447
+ }
448
+ }
449
+ // ── internals ───────────────────────────────────────────────────
450
+ reindex() {
451
+ this.index = new Map(this.rooms.map((r, i) => [r.roomId, i]));
452
+ }
453
+ resubscribe() {
454
+ for (const u of this.roomSubs.splice(0)) u();
455
+ for (const item of this.rooms) {
456
+ const room = this.chat.room(item.roomId);
457
+ this.roomSubs.push(
458
+ room.on("message", (m) => this.onIncoming(item.roomId, m)),
459
+ room.on("syncTick", () => this.scheduleReload()),
460
+ room.on("messagesCleared", () => this.scheduleReload())
461
+ );
462
+ }
463
+ }
464
+ onIncoming(roomId, m) {
465
+ const i = this.index.get(roomId);
466
+ if (i === void 0) return;
467
+ const cur = this.rooms[i];
468
+ const visible = this.opts.isRoomVisible?.(roomId) ?? false;
469
+ const preview = this.opts.previewBuilder?.(m) ?? m.content ?? (m.file_id || m.file || m.files && m.files.length > 0 ? "\u{1F4CE}" : "");
470
+ const updated = {
471
+ ...cur,
472
+ unreadCount: visible ? cur.unreadCount : cur.unreadCount + 1,
473
+ lastMessageId: m.message_id,
474
+ lastMessagePreview: preview,
475
+ lastMessageAtMs: m.created_at
476
+ };
477
+ this.rooms.splice(i, 1);
478
+ this.rooms.unshift(updated);
479
+ this.reindex();
480
+ this.emit();
481
+ }
482
+ scheduleReload() {
483
+ if (this.reloadTimer) clearTimeout(this.reloadTimer);
484
+ this.reloadTimer = setTimeout(() => {
485
+ this.reloadTimer = null;
486
+ if (this.attached) void this.load();
487
+ }, 500);
488
+ }
489
+ };
490
+
491
+ // src/room-settings-store.ts
492
+ var RoomSettingsStore = class {
493
+ chat;
494
+ roomId;
495
+ listeners = /* @__PURE__ */ new Set();
496
+ invites = [];
497
+ joinRequests = [];
498
+ attached = false;
499
+ snapshot = { invites: [], joinRequests: [] };
500
+ constructor(chat, roomId) {
501
+ this.chat = chat;
502
+ this.roomId = roomId;
503
+ }
504
+ get room() {
505
+ return this.chat.room(this.roomId);
506
+ }
507
+ /** No realtime subscriptions — kept for lifecycle symmetry with the
508
+ * other stores (guards in-flight results after unmount). */
509
+ attach() {
510
+ this.attached = true;
511
+ }
512
+ dispose() {
513
+ this.attached = false;
514
+ }
515
+ // ── external-store contract ─────────────────────────────────────
516
+ subscribe = (fn) => {
517
+ this.listeners.add(fn);
518
+ return () => this.listeners.delete(fn);
519
+ };
520
+ getSnapshot = () => this.snapshot;
521
+ emit() {
522
+ this.snapshot = {
523
+ invites: [...this.invites],
524
+ joinRequests: [...this.joinRequests]
525
+ };
526
+ for (const fn of this.listeners) fn();
527
+ }
528
+ // ── notifications ───────────────────────────────────────────────
529
+ /** Convenience mute toggle: `true` → `OFF`, `false` → `ALL`. */
530
+ setMuted(muted) {
531
+ return this.room.setPushTrigger(muted ? "OFF" : "ALL");
532
+ }
533
+ /** Fine-grained per-room push trigger: `ALL` | `MENTION_ONLY` | `OFF`. */
534
+ setPushTrigger(trigger) {
535
+ return this.room.setPushTrigger(trigger);
536
+ }
537
+ // ── invite links ────────────────────────────────────────────────
538
+ /** Fetch the invite list (operator-only server-side). */
539
+ async loadInvites() {
540
+ const list = await this.room.listInvites();
541
+ if (!this.attached) return;
542
+ this.invites = list;
543
+ this.emit();
544
+ }
545
+ /** Mint a new invite link and prepend it to `invites`. */
546
+ async createInvite(opts) {
547
+ const token = await this.room.createInvite(opts);
548
+ if (this.attached) {
549
+ this.invites = [token, ...this.invites];
550
+ this.emit();
551
+ }
552
+ return token;
553
+ }
554
+ /** Revoke a token and drop it from `invites`. */
555
+ async revokeInvite(token) {
556
+ await this.room.revokeInvite(token);
557
+ if (this.attached) {
558
+ this.invites = this.invites.filter((t) => t.token !== token);
559
+ this.emit();
560
+ }
561
+ }
562
+ // ── join requests (operator inbox) ──────────────────────────────
563
+ /** Fetch pending join requests (operator-only server-side). */
564
+ async loadJoinRequests() {
565
+ const page = await this.room.listJoinRequests();
566
+ if (!this.attached) return;
567
+ this.joinRequests = page.items;
568
+ this.emit();
569
+ }
570
+ /** Approve a pending request and drop it from `joinRequests`. */
571
+ async approveJoinRequest(userId, reason) {
572
+ await this.room.approveJoinRequest(userId, reason);
573
+ if (this.attached) {
574
+ this.joinRequests = this.joinRequests.filter((r) => r.userId !== userId);
575
+ this.emit();
576
+ }
577
+ }
578
+ /** Reject a pending request and drop it from `joinRequests`. */
579
+ async rejectJoinRequest(userId, reason) {
580
+ await this.room.rejectJoinRequest(userId, reason);
581
+ if (this.attached) {
582
+ this.joinRequests = this.joinRequests.filter((r) => r.userId !== userId);
583
+ this.emit();
584
+ }
585
+ }
586
+ // ── history / membership ────────────────────────────────────────
587
+ /** Per-user "clear chat" — hides existing messages for the caller only. */
588
+ clearHistory() {
589
+ return this.room.clearHistory();
590
+ }
591
+ /** Leave the room. `deleteMessages`: `"none"` (default) | `"mine"` |
592
+ * `"all"` — see `Room.leave`. */
593
+ leave(options) {
594
+ return this.room.leave(options);
595
+ }
596
+ /** Delete every message the caller sent (without leaving). Returns the
597
+ * cleared count. */
598
+ async deleteMyMessages() {
599
+ const r = await this.room.deleteMyMessages();
600
+ return r.cleared;
601
+ }
602
+ // ── operator moderation ─────────────────────────────────────────
603
+ /** **Destructive, operator-only** — wipe every message in the room. */
604
+ deleteAllMessages() {
605
+ return this.room.deleteAllMessages();
606
+ }
607
+ /** Operator: block non-operator sends (`ROOM_FROZEN` server-side). */
608
+ freeze() {
609
+ return this.room.freeze();
610
+ }
611
+ /** Operator: reopen the room to sends. */
612
+ unfreeze() {
613
+ return this.room.unfreeze();
614
+ }
615
+ /** Operator: toggle open-chat visibility. */
616
+ setPublic(isPublic) {
617
+ return this.room.setPublic(isPublic);
618
+ }
619
+ /** Operator: mute a member (`durationMinutes` null → indefinite). */
620
+ muteMember(userId, durationMinutes) {
621
+ return this.room.muteMember(userId, durationMinutes);
622
+ }
623
+ /** Operator: clear a member's mute. */
624
+ unmuteMember(userId) {
625
+ return this.room.unmuteMember(userId);
626
+ }
627
+ };
628
+
629
+ // src/internal/compare-id.ts
630
+ function idGreater(a, b) {
631
+ if (!a || a === "0") return false;
632
+ if (!b || b === "0") return true;
633
+ try {
634
+ return BigInt(a) > BigInt(b);
635
+ } catch {
636
+ return a > b;
637
+ }
638
+ }
639
+
640
+ // src/room-store.ts
641
+ var uploadSeq = 0;
642
+ var RoomStore = class {
643
+ chat;
644
+ roomId;
645
+ listeners = /* @__PURE__ */ new Set();
646
+ unsubs = [];
647
+ messages = [];
648
+ readWatermarks = {};
649
+ hasMore = false;
650
+ oldestCursor = null;
651
+ loadingMore = false;
652
+ historyRequested = false;
653
+ snapshot;
654
+ attached = false;
655
+ onPageHide = () => this.room.flushMarkRead();
656
+ constructor(chat, roomId) {
657
+ this.chat = chat;
658
+ this.roomId = roomId;
659
+ this.snapshot = {
660
+ messages: [],
661
+ hasMore: false,
662
+ readWatermarks: {}
663
+ };
664
+ }
665
+ /** The live `Room` facade — resolved from the client on every access
666
+ * (get-or-create in a map, cheap) so we never act on an orphan. */
667
+ get room() {
668
+ return this.chat.room(this.roomId);
669
+ }
670
+ /** Subscribe to WS events + page lifecycle. Safe to call repeatedly
671
+ * (no-op while attached) — matches React 18/19 StrictMode's
672
+ * mount → cleanup → mount effect cycle. */
673
+ attach() {
674
+ if (this.attached) return;
675
+ this.attached = true;
676
+ const room = this.room;
677
+ this.unsubs.push(
678
+ room.on("message", (f) => this.onIncoming(f)),
679
+ room.on("messageUpdated", (f) => this.onUpdated(f)),
680
+ room.on("messageDeleted", (f) => this.onDeleted(f)),
681
+ room.on("messagesCleared", (f) => this.onCleared(f)),
682
+ room.on("reactionAdded", (f) => this.onReactionAdded(f)),
683
+ room.on("reactionRemoved", (f) => this.onReactionRemoved(f)),
684
+ room.on("syncTick", (f) => this.onSyncTick(f))
685
+ );
686
+ if (typeof document !== "undefined") {
687
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
688
+ }
689
+ if (typeof window !== "undefined") {
690
+ window.addEventListener("pagehide", this.onPageHide);
691
+ }
692
+ }
693
+ /** Unsubscribe everything and flush the pending read watermark. The store
694
+ * keeps its message state, so a later `attach()` resumes cleanly. */
695
+ dispose() {
696
+ if (!this.attached) return;
697
+ this.attached = false;
698
+ this.room.flushMarkRead();
699
+ for (const u of this.unsubs.splice(0)) u();
700
+ if (typeof document !== "undefined") {
701
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
702
+ }
703
+ if (typeof window !== "undefined") {
704
+ window.removeEventListener("pagehide", this.onPageHide);
705
+ }
706
+ }
707
+ // ── external-store contract ─────────────────────────────────────
708
+ /** `useSyncExternalStore`-compatible subscribe. Returns an unsubscribe fn. */
709
+ subscribe = (fn) => {
710
+ this.listeners.add(fn);
711
+ return () => this.listeners.delete(fn);
712
+ };
713
+ getSnapshot = () => this.snapshot;
714
+ emit() {
715
+ this.snapshot = {
716
+ messages: [...this.messages],
717
+ hasMore: this.hasMore,
718
+ readWatermarks: { ...this.readWatermarks }
719
+ };
720
+ for (const fn of this.listeners) fn();
721
+ }
722
+ // ── read receipts ───────────────────────────────────────────────
723
+ /** Whether `userId` has read `messageId` according to the latest watermark. */
724
+ isReadBy(userId, messageId) {
725
+ const w = this.readWatermarks[userId];
726
+ if (!w) return false;
727
+ return !idGreater(messageId, w);
728
+ }
729
+ /** How many of the given users have read `messageId`. Pass the room's
730
+ * member ids (minus the sender) to get a KakaoTalk-style unread count:
731
+ * `members.length - readCount(...)`. */
732
+ readCount(messageId, userIds) {
733
+ let n = 0;
734
+ for (const u of userIds) if (this.isReadBy(u, messageId)) n++;
735
+ return n;
736
+ }
737
+ // ── history ─────────────────────────────────────────────────────
738
+ /** Load the most recent page of history and prepend it. No-op after the
739
+ * first call (StrictMode double-mount safe) — use `loadMore` for
740
+ * scrollback. */
741
+ async loadHistory(limit = 50) {
742
+ if (this.historyRequested) return;
743
+ this.historyRequested = true;
744
+ const page = await this.room.listRecent(limit);
745
+ this.messages.unshift(...page.items.map(chatMessageFromHistory));
746
+ this.oldestCursor = page.nextCursor;
747
+ this.hasMore = page.hasMore;
748
+ this.emit();
749
+ }
750
+ /** Scrollback: load the page of messages older than what's on screen and
751
+ * prepend it. No-op while a previous call is in flight or when `hasMore`
752
+ * is false. Returns the number of messages added. */
753
+ async loadMore(limit = 50) {
754
+ const cursor = this.oldestCursor;
755
+ if (this.loadingMore || !this.hasMore || cursor == null) return 0;
756
+ this.loadingMore = true;
757
+ try {
758
+ const page = await this.room.listBefore(cursor, limit);
759
+ this.messages.unshift(...page.items.map(chatMessageFromHistory));
760
+ this.oldestCursor = page.nextCursor;
761
+ this.hasMore = page.hasMore;
762
+ this.emit();
763
+ return page.items.length;
764
+ } finally {
765
+ this.loadingMore = false;
766
+ }
767
+ }
768
+ // ── outgoing ────────────────────────────────────────────────────
769
+ /** Send `text` optimistically: a `sending` bubble appears immediately,
770
+ * then flips to `sent` (with the real id) on ack, or `failed` on error.
771
+ *
772
+ * Optional extras ride along: `replyToId` for replies, `customType` /
773
+ * `customMeta` for app-defined conventions (e.g. message priority). */
774
+ send(text, opts) {
775
+ void (async () => {
776
+ let tempId;
777
+ let ackPromise;
778
+ try {
779
+ const sent = await this.room.send({
780
+ content: text,
781
+ ...opts?.replyToId ? { replyToId: opts.replyToId } : {},
782
+ ...opts?.customType ? { customType: opts.customType } : {},
783
+ ...opts?.customMeta ? { customMeta: opts.customMeta } : {}
784
+ });
785
+ tempId = sent.tempId;
786
+ ackPromise = sent.messageId;
787
+ } catch {
788
+ const localId = `failed_${Date.now()}_${uploadSeq++}`;
789
+ this.messages.push({
790
+ ...pendingChatMessage({ tempId: localId, content: text, ...opts }),
791
+ status: "failed"
792
+ });
793
+ this.emit();
794
+ return;
795
+ }
796
+ this.messages.push(
797
+ pendingChatMessage({ tempId, content: text, ...opts })
798
+ );
799
+ this.emit();
800
+ this.bindAck(tempId, ackPromise);
801
+ })();
802
+ }
803
+ /** Send a file optimistically. A FILE bubble with `uploadProgress`
804
+ * appears immediately; progress advances during the S3 upload, then the
805
+ * bubble flips to `sent` on ack (or `failed`). */
806
+ async sendFile(file, opts) {
807
+ const localId = `upload_${Date.now()}_${uploadSeq++}`;
808
+ this.messages.push(
809
+ pendingFileChatMessage({
810
+ tempId: localId,
811
+ name: opts?.name ?? file.name,
812
+ mime: file.type,
813
+ size: file.size
814
+ })
815
+ );
816
+ this.emit();
817
+ try {
818
+ const sent = await this.room.sendFile(file, {
819
+ ...opts ?? {},
820
+ onProgress: (p) => {
821
+ this.mutateByTempId(localId, (m) => ({
822
+ ...m,
823
+ uploadProgress: p.ratio
824
+ }));
825
+ opts?.onProgress?.(p);
826
+ }
827
+ });
828
+ this.mutateByTempId(localId, (m) => ({
829
+ ...m,
830
+ fileId: sent.fileId,
831
+ file: m.file ? { ...m.file, id: sent.fileId } : m.file,
832
+ uploadProgress: null
833
+ }));
834
+ const realId = await sent.messageId;
835
+ this.mutateByTempId(localId, (m) => ({
836
+ ...m,
837
+ id: realId,
838
+ status: "sent"
839
+ }));
840
+ } catch {
841
+ this.mutateByTempId(localId, (m) => ({
842
+ ...m,
843
+ status: "failed",
844
+ uploadProgress: null
845
+ }));
846
+ }
847
+ }
848
+ /** Edit a message's content (server-side; peers get `message_updated`).
849
+ * The local copy updates optimistically. */
850
+ async edit(messageId, content) {
851
+ await this.room.edit(messageId, { content });
852
+ this.mutateById(messageId, (m) => ({
853
+ ...m,
854
+ content,
855
+ editedCount: m.editedCount + 1
856
+ }));
857
+ }
858
+ /** Delete a message. `scope: "ALL"` (default) removes it for everyone;
859
+ * `"MY"` hides it only for the caller. Local copy flips to deleted. */
860
+ async delete(messageId, scope = "ALL") {
861
+ await this.room.delete(messageId, scope);
862
+ this.mutateById(messageId, (m) => ({ ...m, isDeleted: true }));
863
+ }
864
+ /** Add/remove the caller's reaction. Pass `myUserId` so the toggle can
865
+ * tell whether the caller already reacted with `key`. The authoritative
866
+ * state arrives back via the reaction WS events. */
867
+ async toggleReaction(messageId, key, myUserId) {
868
+ const m = this.messages.find((x) => x.id === messageId);
869
+ const mine = m?.reactions.some(
870
+ (r) => r.key === key && r.userIds.includes(myUserId)
871
+ ) ?? false;
872
+ if (mine) {
873
+ await this.room.unreact(messageId, key);
874
+ } else {
875
+ await this.room.react(messageId, key);
876
+ }
877
+ }
878
+ /** Search this room's messages (server-side, content substring match).
879
+ * Returns view models without touching the message list. */
880
+ async search(q, limit = 30) {
881
+ const page = await this.room.search(q, { limit });
882
+ return page.items.map(chatMessageFromHistory);
883
+ }
884
+ /** Mark `messageId` read (debounced inside the SDK). */
885
+ markRead(messageId) {
886
+ this.room.markRead(messageId);
887
+ }
888
+ // ── event handlers ──────────────────────────────────────────────
889
+ onIncoming(f) {
890
+ this.messages.push(chatMessageFromLive(f));
891
+ this.emit();
892
+ }
893
+ onUpdated(f) {
894
+ this.mutateById(f.message_id, (m) => ({
895
+ ...m,
896
+ content: f.content ?? m.content,
897
+ editedCount: m.editedCount + 1
898
+ }));
899
+ }
900
+ onDeleted(f) {
901
+ this.mutateById(f.message_id, (m) => ({ ...m, isDeleted: true }));
902
+ }
903
+ onCleared(f) {
904
+ const upTo = f.up_to_message_id;
905
+ let changed = false;
906
+ this.messages = this.messages.map((m) => {
907
+ if (!m.isDeleted && (upTo == null || !idGreater(m.id, upTo))) {
908
+ changed = true;
909
+ return { ...m, isDeleted: true };
910
+ }
911
+ return m;
912
+ });
913
+ if (changed) this.emit();
914
+ }
915
+ onReactionAdded(f) {
916
+ this.mutateById(f.message_id, (m) => {
917
+ const next = m.reactions.map(
918
+ (r) => r.key === f.reaction_key ? {
919
+ key: r.key,
920
+ userIds: [.../* @__PURE__ */ new Set([...r.userIds, f.user_id])],
921
+ updatedAt: f.updated_at
922
+ } : r
923
+ );
924
+ if (!next.some((r) => r.key === f.reaction_key)) {
925
+ next.push({
926
+ key: f.reaction_key,
927
+ userIds: [f.user_id],
928
+ updatedAt: f.updated_at
929
+ });
930
+ }
931
+ return { ...m, reactions: next };
932
+ });
933
+ }
934
+ onReactionRemoved(f) {
935
+ this.mutateById(f.message_id, (m) => {
936
+ const next = [];
937
+ for (const r of m.reactions) {
938
+ if (r.key !== f.reaction_key) {
939
+ next.push(r);
940
+ continue;
941
+ }
942
+ const users = r.userIds.filter((u) => u !== f.user_id);
943
+ if (users.length > 0) {
944
+ next.push({ key: r.key, userIds: users, updatedAt: f.updated_at });
945
+ }
946
+ }
947
+ return { ...m, reactions: next };
948
+ });
949
+ }
950
+ onSyncTick(f) {
951
+ let changed = false;
952
+ for (const [userId, mid] of Object.entries(f.read_states)) {
953
+ const cur = this.readWatermarks[userId];
954
+ if (!cur || idGreater(mid, cur)) {
955
+ this.readWatermarks[userId] = mid;
956
+ changed = true;
957
+ }
958
+ }
959
+ if (changed) this.emit();
960
+ }
961
+ onVisibilityChange = () => {
962
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
963
+ this.room.flushMarkRead();
964
+ }
965
+ };
966
+ // ── helpers ─────────────────────────────────────────────────────
967
+ bindAck(tempId, messageId) {
968
+ messageId.then((realId) => {
969
+ this.mutateByTempId(tempId, (m) => ({
970
+ ...m,
971
+ id: realId,
972
+ status: "sent"
973
+ }));
974
+ }).catch(() => {
975
+ this.mutateByTempId(tempId, (m) => ({ ...m, status: "failed" }));
976
+ });
977
+ }
978
+ mutateById(id, f) {
979
+ const i = this.messages.findIndex((m) => m.id === id);
980
+ if (i === -1) return;
981
+ this.messages[i] = f(this.messages[i]);
982
+ this.emit();
983
+ }
984
+ mutateByTempId(tempId, f) {
985
+ const i = this.messages.findIndex((m) => m.tempId === tempId);
986
+ if (i === -1) return;
987
+ this.messages[i] = f(this.messages[i]);
988
+ this.emit();
989
+ }
990
+ };
991
+
992
+ // src/use-messages.ts
993
+ var import_react2 = require("react");
994
+ function useMessages(roomId, opts) {
995
+ const chat = useNoveraChat();
996
+ const historyLimit = opts?.historyLimit ?? 50;
997
+ const store = (0, import_react2.useMemo)(() => new RoomStore(chat, roomId), [chat, roomId]);
998
+ (0, import_react2.useEffect)(() => {
999
+ store.attach();
1000
+ void store.loadHistory(historyLimit);
1001
+ return () => store.dispose();
1002
+ }, [store]);
1003
+ const snapshot = (0, import_react2.useSyncExternalStore)(
1004
+ store.subscribe,
1005
+ store.getSnapshot,
1006
+ store.getSnapshot
1007
+ );
1008
+ return { ...snapshot, store };
1009
+ }
1010
+
1011
+ // src/use-member-list.ts
1012
+ var import_react3 = require("react");
1013
+ function useMemberList(roomId) {
1014
+ const chat = useNoveraChat();
1015
+ const store = (0, import_react3.useMemo)(
1016
+ () => new MemberListStore(chat, roomId),
1017
+ [chat, roomId]
1018
+ );
1019
+ (0, import_react3.useEffect)(() => {
1020
+ store.attach();
1021
+ void store.load();
1022
+ return () => store.dispose();
1023
+ }, [store]);
1024
+ const snapshot = (0, import_react3.useSyncExternalStore)(
1025
+ store.subscribe,
1026
+ store.getSnapshot,
1027
+ store.getSnapshot
1028
+ );
1029
+ return { ...snapshot, store };
1030
+ }
1031
+
1032
+ // src/use-room.ts
1033
+ function useRoom(roomId) {
1034
+ return useNoveraChat().room(roomId);
1035
+ }
1036
+
1037
+ // src/use-room-files.ts
1038
+ var import_react4 = require("react");
1039
+ function useRoomFiles(roomId, opts) {
1040
+ const chat = useNoveraChat();
1041
+ const pageSize = opts?.pageSize ?? 50;
1042
+ const store = (0, import_react4.useMemo)(
1043
+ () => new RoomFilesStore(chat, roomId, pageSize),
1044
+ [chat, roomId, pageSize]
1045
+ );
1046
+ (0, import_react4.useEffect)(() => {
1047
+ store.attach();
1048
+ void store.refresh();
1049
+ return () => store.dispose();
1050
+ }, [store]);
1051
+ const snapshot = (0, import_react4.useSyncExternalStore)(
1052
+ store.subscribe,
1053
+ store.getSnapshot,
1054
+ store.getSnapshot
1055
+ );
1056
+ return { ...snapshot, store };
1057
+ }
1058
+
1059
+ // src/use-room-list.ts
1060
+ var import_react5 = require("react");
1061
+ function useRoomList(opts) {
1062
+ const chat = useNoveraChat();
1063
+ const optsRef = (0, import_react5.useRef)(opts);
1064
+ const store = (0, import_react5.useMemo)(
1065
+ () => new RoomListStore(chat, optsRef.current ?? {}),
1066
+ [chat]
1067
+ );
1068
+ (0, import_react5.useEffect)(() => {
1069
+ store.attach();
1070
+ void store.load();
1071
+ return () => store.dispose();
1072
+ }, [store]);
1073
+ const snapshot = (0, import_react5.useSyncExternalStore)(
1074
+ store.subscribe,
1075
+ store.getSnapshot,
1076
+ store.getSnapshot
1077
+ );
1078
+ return { ...snapshot, store };
1079
+ }
1080
+
1081
+ // src/use-room-settings.ts
1082
+ var import_react6 = require("react");
1083
+ function useRoomSettings(roomId) {
1084
+ const chat = useNoveraChat();
1085
+ const store = (0, import_react6.useMemo)(
1086
+ () => new RoomSettingsStore(chat, roomId),
1087
+ [chat, roomId]
1088
+ );
1089
+ (0, import_react6.useEffect)(() => {
1090
+ store.attach();
1091
+ return () => store.dispose();
1092
+ }, [store]);
1093
+ const snapshot = (0, import_react6.useSyncExternalStore)(
1094
+ store.subscribe,
1095
+ store.getSnapshot,
1096
+ store.getSnapshot
1097
+ );
1098
+ return { ...snapshot, store };
1099
+ }
1100
+
1101
+ // src/use-typing.ts
1102
+ var import_react7 = require("react");
1103
+ function useTyping(roomId, opts) {
1104
+ const chat = useNoveraChat();
1105
+ const timeoutMs = opts?.timeoutMs ?? 5e3;
1106
+ const [typingUserIds, setTypingUserIds] = (0, import_react7.useState)([]);
1107
+ const timers = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1108
+ (0, import_react7.useEffect)(() => {
1109
+ const room = chat.room(roomId);
1110
+ const timerMap = timers.current;
1111
+ const stop = (userId) => {
1112
+ const t = timerMap.get(userId);
1113
+ if (t) clearTimeout(t);
1114
+ timerMap.delete(userId);
1115
+ setTypingUserIds(
1116
+ (cur) => cur.includes(userId) ? cur.filter((u) => u !== userId) : cur
1117
+ );
1118
+ };
1119
+ const start = (userId) => {
1120
+ const t = timerMap.get(userId);
1121
+ if (t) clearTimeout(t);
1122
+ timerMap.set(userId, setTimeout(() => stop(userId), timeoutMs));
1123
+ setTypingUserIds(
1124
+ (cur) => cur.includes(userId) ? cur : [...cur, userId]
1125
+ );
1126
+ };
1127
+ const unsub = room.on("typing", (f) => {
1128
+ if (f.is_typing) start(f.user_id);
1129
+ else stop(f.user_id);
1130
+ });
1131
+ return () => {
1132
+ unsub();
1133
+ for (const t of timerMap.values()) clearTimeout(t);
1134
+ timerMap.clear();
1135
+ setTypingUserIds([]);
1136
+ };
1137
+ }, [chat, roomId, timeoutMs]);
1138
+ return {
1139
+ typingUserIds,
1140
+ isAnyoneTyping: typingUserIds.length > 0,
1141
+ setTyping: (isTyping) => chat.room(roomId).setTyping(isTyping)
1142
+ };
1143
+ }
1144
+
1145
+ // src/use-unread.ts
1146
+ var import_react8 = require("react");
1147
+ function useUnread(opts) {
1148
+ const chat = useNoveraChat();
1149
+ const refreshIntervalMs = opts?.refreshIntervalMs;
1150
+ const [summary, setSummary] = (0, import_react8.useState)(null);
1151
+ const refresh = (0, import_react8.useCallback)(async () => {
1152
+ setSummary(await chat.unreadSummary());
1153
+ }, [chat]);
1154
+ (0, import_react8.useEffect)(() => {
1155
+ let alive = true;
1156
+ const tick = async () => {
1157
+ try {
1158
+ const s = await chat.unreadSummary();
1159
+ if (alive) setSummary(s);
1160
+ } catch {
1161
+ }
1162
+ };
1163
+ void tick();
1164
+ const timer = refreshIntervalMs != null && refreshIntervalMs > 0 ? setInterval(() => void tick(), refreshIntervalMs) : null;
1165
+ return () => {
1166
+ alive = false;
1167
+ if (timer) clearInterval(timer);
1168
+ };
1169
+ }, [chat, refreshIntervalMs]);
1170
+ return { total: summary?.total ?? 0, summary, refresh };
1171
+ }
1172
+ // Annotate the CommonJS export names for ESM import in node:
1173
+ 0 && (module.exports = {
1174
+ MemberListStore,
1175
+ NoveraChatProvider,
1176
+ RoomFilesStore,
1177
+ RoomListStore,
1178
+ RoomSettingsStore,
1179
+ RoomStore,
1180
+ chatMessageFromHistory,
1181
+ chatMessageFromLive,
1182
+ useMemberList,
1183
+ useMessages,
1184
+ useNoveraChat,
1185
+ useRoom,
1186
+ useRoomFiles,
1187
+ useRoomList,
1188
+ useRoomSettings,
1189
+ useTyping,
1190
+ useUnread,
1191
+ ...require("@noverachat/sdk-web")
1192
+ });