@noverachat/sdk-react 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +2 -0
- package/README.md +2 -0
- package/dist/index.cjs +52 -370
- package/dist/index.d.cts +31 -95
- package/dist/index.d.ts +31 -95
- package/dist/index.js +52 -365
- package/package.json +2 -2
- package/src/chat-message.ts +10 -208
- package/src/room-list-store.ts +4 -4
- package/src/room-store.ts +57 -293
package/dist/index.js
CHANGED
|
@@ -2,119 +2,12 @@
|
|
|
2
2
|
export * from "@noverachat/sdk-web";
|
|
3
3
|
|
|
4
4
|
// src/chat-message.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
name: w.name ?? null,
|
|
12
|
-
width: w.width ?? null,
|
|
13
|
-
height: w.height ?? null,
|
|
14
|
-
durationSec: w.duration_sec ?? null,
|
|
15
|
-
thumbnailStatus: w.thumbnail_status,
|
|
16
|
-
forwardBlocked: Boolean(w.forward_blocked)
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function chatMessageFromHistory(m) {
|
|
20
|
-
return {
|
|
21
|
-
id: m.messageId,
|
|
22
|
-
senderId: m.senderId ?? null,
|
|
23
|
-
content: m.content ?? null,
|
|
24
|
-
createdAtMs: m.createdAtMs,
|
|
25
|
-
status: "sent",
|
|
26
|
-
messageType: m.messageType,
|
|
27
|
-
sender: m.sender ?? null,
|
|
28
|
-
customType: m.customType ?? null,
|
|
29
|
-
fileId: m.fileId ?? null,
|
|
30
|
-
file: m.file ?? null,
|
|
31
|
-
files: m.files ?? null,
|
|
32
|
-
replyToId: m.replyToId ?? null,
|
|
33
|
-
reactions: m.reactions ?? [],
|
|
34
|
-
customMeta: m.customMeta ?? null,
|
|
35
|
-
editedCount: m.editedCount ?? 0,
|
|
36
|
-
uploadProgress: null,
|
|
37
|
-
tempId: null,
|
|
38
|
-
isDeleted: m.deletedAt != null
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function chatMessageFromLive(f) {
|
|
42
|
-
const data = f.data ?? null;
|
|
43
|
-
const customMeta = data && typeof data === "object" && "_customMeta" in data ? data._customMeta : null;
|
|
44
|
-
return {
|
|
45
|
-
id: f.message_id,
|
|
46
|
-
senderId: f.sender_id,
|
|
47
|
-
content: f.content ?? null,
|
|
48
|
-
createdAtMs: f.created_at,
|
|
49
|
-
status: "sent",
|
|
50
|
-
messageType: f.message_type,
|
|
51
|
-
sender: null,
|
|
52
|
-
customType: f.custom_type ?? null,
|
|
53
|
-
fileId: f.file_id ?? null,
|
|
54
|
-
file: f.file ? fileInlineFromWire(f.file) : null,
|
|
55
|
-
files: f.files ? f.files.map(fileInlineFromWire) : null,
|
|
56
|
-
replyToId: f.reply_to_id ?? null,
|
|
57
|
-
reactions: [],
|
|
58
|
-
customMeta,
|
|
59
|
-
editedCount: 0,
|
|
60
|
-
uploadProgress: null,
|
|
61
|
-
tempId: null,
|
|
62
|
-
isDeleted: false
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
function pendingChatMessage(p) {
|
|
66
|
-
return {
|
|
67
|
-
id: p.tempId,
|
|
68
|
-
senderId: null,
|
|
69
|
-
content: p.content,
|
|
70
|
-
createdAtMs: Date.now(),
|
|
71
|
-
status: "sending",
|
|
72
|
-
messageType: "TEXT",
|
|
73
|
-
sender: null,
|
|
74
|
-
customType: p.customType ?? null,
|
|
75
|
-
fileId: null,
|
|
76
|
-
file: null,
|
|
77
|
-
files: null,
|
|
78
|
-
replyToId: p.replyToId ?? null,
|
|
79
|
-
reactions: [],
|
|
80
|
-
customMeta: p.customMeta ?? null,
|
|
81
|
-
editedCount: 0,
|
|
82
|
-
uploadProgress: null,
|
|
83
|
-
tempId: p.tempId,
|
|
84
|
-
isDeleted: false
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function pendingFileChatMessage(p) {
|
|
88
|
-
const mime = p.mime ?? "application/octet-stream";
|
|
89
|
-
return {
|
|
90
|
-
id: p.tempId,
|
|
91
|
-
senderId: null,
|
|
92
|
-
content: null,
|
|
93
|
-
createdAtMs: Date.now(),
|
|
94
|
-
status: "sending",
|
|
95
|
-
messageType: "FILE",
|
|
96
|
-
sender: null,
|
|
97
|
-
customType: null,
|
|
98
|
-
fileId: null,
|
|
99
|
-
file: {
|
|
100
|
-
id: "",
|
|
101
|
-
kind: mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : "file",
|
|
102
|
-
mime,
|
|
103
|
-
size: p.size ?? 0,
|
|
104
|
-
name: p.name,
|
|
105
|
-
thumbnailStatus: "pending",
|
|
106
|
-
forwardBlocked: false
|
|
107
|
-
},
|
|
108
|
-
files: null,
|
|
109
|
-
replyToId: null,
|
|
110
|
-
reactions: [],
|
|
111
|
-
customMeta: null,
|
|
112
|
-
editedCount: 0,
|
|
113
|
-
uploadProgress: 0,
|
|
114
|
-
tempId: p.tempId,
|
|
115
|
-
isDeleted: false
|
|
116
|
-
};
|
|
117
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
chatMessageFromHistory,
|
|
7
|
+
chatMessageFromLive,
|
|
8
|
+
pendingChatMessage,
|
|
9
|
+
pendingFileChatMessage
|
|
10
|
+
} from "@noverachat/sdk-web";
|
|
118
11
|
|
|
119
12
|
// src/context.tsx
|
|
120
13
|
import {
|
|
@@ -321,7 +214,7 @@ function itemFromUnread(r) {
|
|
|
321
214
|
lastMessagePreview: r.lastMessagePreview ?? null,
|
|
322
215
|
memberCount: r.memberCount ?? 0,
|
|
323
216
|
members,
|
|
324
|
-
lastMessageAtMs: null,
|
|
217
|
+
lastMessageAtMs: r.lastMessageAt ?? null,
|
|
325
218
|
anyMemberOnline: members.some((m) => m.isOnline === true)
|
|
326
219
|
};
|
|
327
220
|
}
|
|
@@ -590,29 +483,13 @@ var RoomSettingsStore = class {
|
|
|
590
483
|
}
|
|
591
484
|
};
|
|
592
485
|
|
|
593
|
-
// src/internal/compare-id.ts
|
|
594
|
-
function idGreater(a, b) {
|
|
595
|
-
if (!a || a === "0") return false;
|
|
596
|
-
if (!b || b === "0") return true;
|
|
597
|
-
try {
|
|
598
|
-
return BigInt(a) > BigInt(b);
|
|
599
|
-
} catch {
|
|
600
|
-
return a > b;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
486
|
// src/room-store.ts
|
|
605
|
-
var uploadSeq = 0;
|
|
606
487
|
var RoomStore = class {
|
|
607
488
|
chat;
|
|
608
489
|
roomId;
|
|
609
490
|
listeners = /* @__PURE__ */ new Set();
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
readWatermarks = {};
|
|
613
|
-
hasMore = false;
|
|
614
|
-
oldestCursor = null;
|
|
615
|
-
loadingMore = false;
|
|
491
|
+
col = null;
|
|
492
|
+
colUnsub = null;
|
|
616
493
|
historyRequested = false;
|
|
617
494
|
snapshot;
|
|
618
495
|
attached = false;
|
|
@@ -631,22 +508,25 @@ var RoomStore = class {
|
|
|
631
508
|
get room() {
|
|
632
509
|
return this.chat.room(this.roomId);
|
|
633
510
|
}
|
|
634
|
-
/**
|
|
635
|
-
*
|
|
511
|
+
/** The backing collection, bound to the CURRENT facade. Recreated when
|
|
512
|
+
* the facade was dropped (post-`chat.disconnect()`) — the old one would
|
|
513
|
+
* never receive dispatches again. */
|
|
514
|
+
collection() {
|
|
515
|
+
const facade = this.room;
|
|
516
|
+
if (this.col && this.col.room === facade) return this.col;
|
|
517
|
+
this.colUnsub?.();
|
|
518
|
+
this.col = facade.collection();
|
|
519
|
+
this.colUnsub = this.col.on("change", () => this.rebuild());
|
|
520
|
+
this.rebuild();
|
|
521
|
+
return this.col;
|
|
522
|
+
}
|
|
523
|
+
/** Subscribe page lifecycle + ensure the collection is live. Safe to call
|
|
524
|
+
* repeatedly (no-op while attached) — matches React 18/19 StrictMode's
|
|
636
525
|
* mount → cleanup → mount effect cycle. */
|
|
637
526
|
attach() {
|
|
638
527
|
if (this.attached) return;
|
|
639
528
|
this.attached = true;
|
|
640
|
-
|
|
641
|
-
this.unsubs.push(
|
|
642
|
-
room.on("message", (f) => this.onIncoming(f)),
|
|
643
|
-
room.on("messageUpdated", (f) => this.onUpdated(f)),
|
|
644
|
-
room.on("messageDeleted", (f) => this.onDeleted(f)),
|
|
645
|
-
room.on("messagesCleared", (f) => this.onCleared(f)),
|
|
646
|
-
room.on("reactionAdded", (f) => this.onReactionAdded(f)),
|
|
647
|
-
room.on("reactionRemoved", (f) => this.onReactionRemoved(f)),
|
|
648
|
-
room.on("syncTick", (f) => this.onSyncTick(f))
|
|
649
|
-
);
|
|
529
|
+
this.collection();
|
|
650
530
|
if (typeof document !== "undefined") {
|
|
651
531
|
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
652
532
|
}
|
|
@@ -654,13 +534,13 @@ var RoomStore = class {
|
|
|
654
534
|
window.addEventListener("pagehide", this.onPageHide);
|
|
655
535
|
}
|
|
656
536
|
}
|
|
657
|
-
/**
|
|
658
|
-
*
|
|
537
|
+
/** Detach page listeners and flush the pending read watermark. The
|
|
538
|
+
* collection (and its state) survives, so a later `attach()` resumes
|
|
539
|
+
* cleanly — StrictMode's mount → cleanup → mount cycle keeps messages. */
|
|
659
540
|
dispose() {
|
|
660
541
|
if (!this.attached) return;
|
|
661
542
|
this.attached = false;
|
|
662
543
|
this.room.flushMarkRead();
|
|
663
|
-
for (const u of this.unsubs.splice(0)) u();
|
|
664
544
|
if (typeof document !== "undefined") {
|
|
665
545
|
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
666
546
|
}
|
|
@@ -675,59 +555,42 @@ var RoomStore = class {
|
|
|
675
555
|
return () => this.listeners.delete(fn);
|
|
676
556
|
};
|
|
677
557
|
getSnapshot = () => this.snapshot;
|
|
678
|
-
|
|
558
|
+
rebuild() {
|
|
559
|
+
const col = this.col;
|
|
560
|
+
if (!col) return;
|
|
679
561
|
this.snapshot = {
|
|
680
|
-
messages: [...
|
|
681
|
-
hasMore:
|
|
682
|
-
readWatermarks: { ...
|
|
562
|
+
messages: [...col.messages],
|
|
563
|
+
hasMore: col.hasMore,
|
|
564
|
+
readWatermarks: { ...col.readWatermarks }
|
|
683
565
|
};
|
|
684
566
|
for (const fn of this.listeners) fn();
|
|
685
567
|
}
|
|
686
568
|
// ── read receipts ───────────────────────────────────────────────
|
|
687
569
|
/** Whether `userId` has read `messageId` according to the latest watermark. */
|
|
688
570
|
isReadBy(userId, messageId) {
|
|
689
|
-
|
|
690
|
-
if (!w) return false;
|
|
691
|
-
return !idGreater(messageId, w);
|
|
571
|
+
return this.collection().isReadBy(userId, messageId);
|
|
692
572
|
}
|
|
693
573
|
/** How many of the given users have read `messageId`. Pass the room's
|
|
694
574
|
* member ids (minus the sender) to get a KakaoTalk-style unread count:
|
|
695
575
|
* `members.length - readCount(...)`. */
|
|
696
576
|
readCount(messageId, userIds) {
|
|
697
|
-
|
|
698
|
-
for (const u of userIds) if (this.isReadBy(u, messageId)) n++;
|
|
699
|
-
return n;
|
|
577
|
+
return this.collection().readCount(messageId, userIds);
|
|
700
578
|
}
|
|
701
579
|
// ── history ─────────────────────────────────────────────────────
|
|
702
|
-
/** Load the most recent page of history
|
|
703
|
-
*
|
|
704
|
-
*
|
|
580
|
+
/** Load the most recent page of history. No-op after the first call
|
|
581
|
+
* (StrictMode double-mount safe) — use `loadMore` for scrollback.
|
|
582
|
+
* 캐시(`ClientOptions.cacheStore`)가 켜져 있으면 스냅샷 즉시 렌더 →
|
|
583
|
+
* API 도착 시 통째 교체가 컬렉션 안에서 일어난다. */
|
|
705
584
|
async loadHistory(limit = 50) {
|
|
706
585
|
if (this.historyRequested) return;
|
|
707
586
|
this.historyRequested = true;
|
|
708
|
-
|
|
709
|
-
this.messages.unshift(...page.items.map(chatMessageFromHistory));
|
|
710
|
-
this.oldestCursor = page.nextCursor;
|
|
711
|
-
this.hasMore = page.hasMore;
|
|
712
|
-
this.emit();
|
|
587
|
+
await this.collection().load(limit);
|
|
713
588
|
}
|
|
714
589
|
/** Scrollback: load the page of messages older than what's on screen and
|
|
715
590
|
* prepend it. No-op while a previous call is in flight or when `hasMore`
|
|
716
591
|
* is false. Returns the number of messages added. */
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (this.loadingMore || !this.hasMore || cursor == null) return 0;
|
|
720
|
-
this.loadingMore = true;
|
|
721
|
-
try {
|
|
722
|
-
const page = await this.room.listBefore(cursor, limit);
|
|
723
|
-
this.messages.unshift(...page.items.map(chatMessageFromHistory));
|
|
724
|
-
this.oldestCursor = page.nextCursor;
|
|
725
|
-
this.hasMore = page.hasMore;
|
|
726
|
-
this.emit();
|
|
727
|
-
return page.items.length;
|
|
728
|
-
} finally {
|
|
729
|
-
this.loadingMore = false;
|
|
730
|
-
}
|
|
592
|
+
loadMore(limit = 50) {
|
|
593
|
+
return this.collection().loadMore(limit);
|
|
731
594
|
}
|
|
732
595
|
// ── outgoing ────────────────────────────────────────────────────
|
|
733
596
|
/** Send `text` optimistically: a `sending` bubble appears immediately,
|
|
@@ -736,108 +599,29 @@ var RoomStore = class {
|
|
|
736
599
|
* Optional extras ride along: `replyToId` for replies, `customType` /
|
|
737
600
|
* `customMeta` for app-defined conventions (e.g. message priority). */
|
|
738
601
|
send(text, opts) {
|
|
739
|
-
|
|
740
|
-
let tempId;
|
|
741
|
-
let ackPromise;
|
|
742
|
-
try {
|
|
743
|
-
const sent = await this.room.send({
|
|
744
|
-
content: text,
|
|
745
|
-
...opts?.replyToId ? { replyToId: opts.replyToId } : {},
|
|
746
|
-
...opts?.customType ? { customType: opts.customType } : {},
|
|
747
|
-
...opts?.customMeta ? { customMeta: opts.customMeta } : {}
|
|
748
|
-
});
|
|
749
|
-
tempId = sent.tempId;
|
|
750
|
-
ackPromise = sent.messageId;
|
|
751
|
-
} catch {
|
|
752
|
-
const localId = `failed_${Date.now()}_${uploadSeq++}`;
|
|
753
|
-
this.messages.push({
|
|
754
|
-
...pendingChatMessage({ tempId: localId, content: text, ...opts }),
|
|
755
|
-
status: "failed"
|
|
756
|
-
});
|
|
757
|
-
this.emit();
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
this.messages.push(
|
|
761
|
-
pendingChatMessage({ tempId, content: text, ...opts })
|
|
762
|
-
);
|
|
763
|
-
this.emit();
|
|
764
|
-
this.bindAck(tempId, ackPromise);
|
|
765
|
-
})();
|
|
602
|
+
this.collection().send(text, opts);
|
|
766
603
|
}
|
|
767
604
|
/** Send a file optimistically. A FILE bubble with `uploadProgress`
|
|
768
605
|
* appears immediately; progress advances during the S3 upload, then the
|
|
769
606
|
* bubble flips to `sent` on ack (or `failed`). */
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
this.messages.push(
|
|
773
|
-
pendingFileChatMessage({
|
|
774
|
-
tempId: localId,
|
|
775
|
-
name: opts?.name ?? file.name,
|
|
776
|
-
mime: file.type,
|
|
777
|
-
size: file.size
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
this.emit();
|
|
781
|
-
try {
|
|
782
|
-
const sent = await this.room.sendFile(file, {
|
|
783
|
-
...opts ?? {},
|
|
784
|
-
onProgress: (p) => {
|
|
785
|
-
this.mutateByTempId(localId, (m) => ({
|
|
786
|
-
...m,
|
|
787
|
-
uploadProgress: p.ratio
|
|
788
|
-
}));
|
|
789
|
-
opts?.onProgress?.(p);
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
this.mutateByTempId(localId, (m) => ({
|
|
793
|
-
...m,
|
|
794
|
-
fileId: sent.fileId,
|
|
795
|
-
file: m.file ? { ...m.file, id: sent.fileId } : m.file,
|
|
796
|
-
uploadProgress: null
|
|
797
|
-
}));
|
|
798
|
-
const realId = await sent.messageId;
|
|
799
|
-
this.mutateByTempId(localId, (m) => ({
|
|
800
|
-
...m,
|
|
801
|
-
id: realId,
|
|
802
|
-
status: "sent"
|
|
803
|
-
}));
|
|
804
|
-
} catch {
|
|
805
|
-
this.mutateByTempId(localId, (m) => ({
|
|
806
|
-
...m,
|
|
807
|
-
status: "failed",
|
|
808
|
-
uploadProgress: null
|
|
809
|
-
}));
|
|
810
|
-
}
|
|
607
|
+
sendFile(file, opts) {
|
|
608
|
+
return this.collection().sendFile(file, opts);
|
|
811
609
|
}
|
|
812
610
|
/** Edit a message's content (server-side; peers get `message_updated`).
|
|
813
611
|
* The local copy updates optimistically. */
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
this.mutateById(messageId, (m) => ({
|
|
817
|
-
...m,
|
|
818
|
-
content,
|
|
819
|
-
editedCount: m.editedCount + 1
|
|
820
|
-
}));
|
|
612
|
+
edit(messageId, content) {
|
|
613
|
+
return this.collection().edit(messageId, content);
|
|
821
614
|
}
|
|
822
615
|
/** Delete a message. `scope: "ALL"` (default) removes it for everyone;
|
|
823
616
|
* `"MY"` hides it only for the caller. Local copy flips to deleted. */
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
this.mutateById(messageId, (m) => ({ ...m, isDeleted: true }));
|
|
617
|
+
delete(messageId, scope = "ALL") {
|
|
618
|
+
return this.collection().delete(messageId, scope);
|
|
827
619
|
}
|
|
828
620
|
/** Add/remove the caller's reaction. Pass `myUserId` so the toggle can
|
|
829
621
|
* tell whether the caller already reacted with `key`. The authoritative
|
|
830
622
|
* state arrives back via the reaction WS events. */
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const mine = m?.reactions.some(
|
|
834
|
-
(r) => r.key === key && r.userIds.includes(myUserId)
|
|
835
|
-
) ?? false;
|
|
836
|
-
if (mine) {
|
|
837
|
-
await this.room.unreact(messageId, key);
|
|
838
|
-
} else {
|
|
839
|
-
await this.room.react(messageId, key);
|
|
840
|
-
}
|
|
623
|
+
toggleReaction(messageId, key, myUserId) {
|
|
624
|
+
return this.collection().toggleReaction(messageId, key, myUserId);
|
|
841
625
|
}
|
|
842
626
|
/** Search this room's messages (server-side, content substring match).
|
|
843
627
|
* Returns view models without touching the message list. */
|
|
@@ -847,110 +631,13 @@ var RoomStore = class {
|
|
|
847
631
|
}
|
|
848
632
|
/** Mark `messageId` read (debounced inside the SDK). */
|
|
849
633
|
markRead(messageId) {
|
|
850
|
-
this.
|
|
851
|
-
}
|
|
852
|
-
// ── event handlers ──────────────────────────────────────────────
|
|
853
|
-
onIncoming(f) {
|
|
854
|
-
this.messages.push(chatMessageFromLive(f));
|
|
855
|
-
this.emit();
|
|
856
|
-
}
|
|
857
|
-
onUpdated(f) {
|
|
858
|
-
this.mutateById(f.message_id, (m) => ({
|
|
859
|
-
...m,
|
|
860
|
-
content: f.content ?? m.content,
|
|
861
|
-
editedCount: m.editedCount + 1
|
|
862
|
-
}));
|
|
863
|
-
}
|
|
864
|
-
onDeleted(f) {
|
|
865
|
-
this.mutateById(f.message_id, (m) => ({ ...m, isDeleted: true }));
|
|
866
|
-
}
|
|
867
|
-
onCleared(f) {
|
|
868
|
-
const upTo = f.up_to_message_id;
|
|
869
|
-
let changed = false;
|
|
870
|
-
this.messages = this.messages.map((m) => {
|
|
871
|
-
if (!m.isDeleted && (upTo == null || !idGreater(m.id, upTo))) {
|
|
872
|
-
changed = true;
|
|
873
|
-
return { ...m, isDeleted: true };
|
|
874
|
-
}
|
|
875
|
-
return m;
|
|
876
|
-
});
|
|
877
|
-
if (changed) this.emit();
|
|
878
|
-
}
|
|
879
|
-
onReactionAdded(f) {
|
|
880
|
-
this.mutateById(f.message_id, (m) => {
|
|
881
|
-
const next = m.reactions.map(
|
|
882
|
-
(r) => r.key === f.reaction_key ? {
|
|
883
|
-
key: r.key,
|
|
884
|
-
userIds: [.../* @__PURE__ */ new Set([...r.userIds, f.user_id])],
|
|
885
|
-
updatedAt: f.updated_at
|
|
886
|
-
} : r
|
|
887
|
-
);
|
|
888
|
-
if (!next.some((r) => r.key === f.reaction_key)) {
|
|
889
|
-
next.push({
|
|
890
|
-
key: f.reaction_key,
|
|
891
|
-
userIds: [f.user_id],
|
|
892
|
-
updatedAt: f.updated_at
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
return { ...m, reactions: next };
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
onReactionRemoved(f) {
|
|
899
|
-
this.mutateById(f.message_id, (m) => {
|
|
900
|
-
const next = [];
|
|
901
|
-
for (const r of m.reactions) {
|
|
902
|
-
if (r.key !== f.reaction_key) {
|
|
903
|
-
next.push(r);
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const users = r.userIds.filter((u) => u !== f.user_id);
|
|
907
|
-
if (users.length > 0) {
|
|
908
|
-
next.push({ key: r.key, userIds: users, updatedAt: f.updated_at });
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return { ...m, reactions: next };
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
onSyncTick(f) {
|
|
915
|
-
let changed = false;
|
|
916
|
-
for (const [userId, mid] of Object.entries(f.read_states)) {
|
|
917
|
-
const cur = this.readWatermarks[userId];
|
|
918
|
-
if (!cur || idGreater(mid, cur)) {
|
|
919
|
-
this.readWatermarks[userId] = mid;
|
|
920
|
-
changed = true;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
if (changed) this.emit();
|
|
634
|
+
this.collection().markRead(messageId);
|
|
924
635
|
}
|
|
925
636
|
onVisibilityChange = () => {
|
|
926
637
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
927
638
|
this.room.flushMarkRead();
|
|
928
639
|
}
|
|
929
640
|
};
|
|
930
|
-
// ── helpers ─────────────────────────────────────────────────────
|
|
931
|
-
bindAck(tempId, messageId) {
|
|
932
|
-
messageId.then((realId) => {
|
|
933
|
-
this.mutateByTempId(tempId, (m) => ({
|
|
934
|
-
...m,
|
|
935
|
-
id: realId,
|
|
936
|
-
status: "sent"
|
|
937
|
-
}));
|
|
938
|
-
}).catch(() => {
|
|
939
|
-
this.mutateByTempId(tempId, (m) => ({ ...m, status: "failed" }));
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
mutateById(id, f) {
|
|
943
|
-
const i = this.messages.findIndex((m) => m.id === id);
|
|
944
|
-
if (i === -1) return;
|
|
945
|
-
this.messages[i] = f(this.messages[i]);
|
|
946
|
-
this.emit();
|
|
947
|
-
}
|
|
948
|
-
mutateByTempId(tempId, f) {
|
|
949
|
-
const i = this.messages.findIndex((m) => m.tempId === tempId);
|
|
950
|
-
if (i === -1) return;
|
|
951
|
-
this.messages[i] = f(this.messages[i]);
|
|
952
|
-
this.emit();
|
|
953
|
-
}
|
|
954
641
|
};
|
|
955
642
|
|
|
956
643
|
// src/use-messages.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noverachat/sdk-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "React hooks/bindings for NoveraChat — @noverachat/sdk-web 위에 얹는 얇은 래퍼",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"react": ">=18"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@noverachat/sdk-web": "0.
|
|
27
|
+
"@noverachat/sdk-web": "0.5.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/react": "^19.0.0",
|