@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/README.ko.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
NoveraChat용 React 훅/바인딩 — [`@noverachat/sdk-web`](../sdk-web)(헤드리스 `NoveraChat` / `Room` 코어) 위의 얇은 레이어. 채팅 로직을 재구현하지 않고 채팅 UI도 제공하지 않는다 — 이 패키지는 글루만 담당한다: 라이프사이클을 관리하는 프로바이더와, SDK 이벤트를 React 상태로 옮기는 훅들.
|
|
6
6
|
|
|
7
|
+
> 팁: 클라이언트 옵션에 [`cacheStore`](../sdk-web/docs/core/caching.ko.md)만 주입하면 `useMessages`가 콜드 스타트에서 마지막 히스토리를 즉시 렌더한다(스냅샷 캐시) — 화면 코드 변경 없음.
|
|
8
|
+
|
|
7
9
|
## 설치
|
|
8
10
|
|
|
9
11
|
```sh
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
React hooks/bindings for NoveraChat — a thin layer over [`@noverachat/sdk-web`](../sdk-web) (the headless `NoveraChat` / `Room` core). It does NOT reimplement chat logic and it does NOT ship chat UI — this package is only the glue: a lifecycle-managed provider and hooks that turn the SDK's events into React state.
|
|
6
6
|
|
|
7
|
+
> Tip: inject a [`cacheStore`](../sdk-web/docs/core/caching.md) in the client options and `useMessages` renders the last known history instantly on cold start (snapshot cache) — zero screen-code changes.
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```sh
|
package/dist/index.cjs
CHANGED
|
@@ -27,8 +27,8 @@ __export(index_exports, {
|
|
|
27
27
|
RoomListStore: () => RoomListStore,
|
|
28
28
|
RoomSettingsStore: () => RoomSettingsStore,
|
|
29
29
|
RoomStore: () => RoomStore,
|
|
30
|
-
chatMessageFromHistory: () => chatMessageFromHistory,
|
|
31
|
-
chatMessageFromLive: () => chatMessageFromLive,
|
|
30
|
+
chatMessageFromHistory: () => import_sdk_web.chatMessageFromHistory,
|
|
31
|
+
chatMessageFromLive: () => import_sdk_web.chatMessageFromLive,
|
|
32
32
|
useMemberList: () => useMemberList,
|
|
33
33
|
useMessages: () => useMessages,
|
|
34
34
|
useNoveraChat: () => useNoveraChat,
|
|
@@ -43,127 +43,15 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
__reExport(index_exports, require("@noverachat/sdk-web"), module.exports);
|
|
44
44
|
|
|
45
45
|
// src/chat-message.ts
|
|
46
|
-
|
|
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
|
-
}
|
|
46
|
+
var import_sdk_web = require("@noverachat/sdk-web");
|
|
159
47
|
|
|
160
48
|
// src/context.tsx
|
|
161
49
|
var import_react = require("react");
|
|
162
|
-
var
|
|
50
|
+
var import_sdk_web2 = require("@noverachat/sdk-web");
|
|
163
51
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
164
52
|
var NoveraChatContext = (0, import_react.createContext)(null);
|
|
165
53
|
function NoveraChatProvider(props) {
|
|
166
|
-
const [chat] = (0, import_react.useState)(() => new
|
|
54
|
+
const [chat] = (0, import_react.useState)(() => new import_sdk_web2.NoveraChat(props.options));
|
|
167
55
|
(0, import_react.useEffect)(() => {
|
|
168
56
|
void chat.connect();
|
|
169
57
|
return () => chat.disconnect();
|
|
@@ -357,7 +245,7 @@ function itemFromUnread(r) {
|
|
|
357
245
|
lastMessagePreview: r.lastMessagePreview ?? null,
|
|
358
246
|
memberCount: r.memberCount ?? 0,
|
|
359
247
|
members,
|
|
360
|
-
lastMessageAtMs: null,
|
|
248
|
+
lastMessageAtMs: r.lastMessageAt ?? null,
|
|
361
249
|
anyMemberOnline: members.some((m) => m.isOnline === true)
|
|
362
250
|
};
|
|
363
251
|
}
|
|
@@ -626,29 +514,13 @@ var RoomSettingsStore = class {
|
|
|
626
514
|
}
|
|
627
515
|
};
|
|
628
516
|
|
|
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
517
|
// src/room-store.ts
|
|
641
|
-
var uploadSeq = 0;
|
|
642
518
|
var RoomStore = class {
|
|
643
519
|
chat;
|
|
644
520
|
roomId;
|
|
645
521
|
listeners = /* @__PURE__ */ new Set();
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
readWatermarks = {};
|
|
649
|
-
hasMore = false;
|
|
650
|
-
oldestCursor = null;
|
|
651
|
-
loadingMore = false;
|
|
522
|
+
col = null;
|
|
523
|
+
colUnsub = null;
|
|
652
524
|
historyRequested = false;
|
|
653
525
|
snapshot;
|
|
654
526
|
attached = false;
|
|
@@ -667,22 +539,25 @@ var RoomStore = class {
|
|
|
667
539
|
get room() {
|
|
668
540
|
return this.chat.room(this.roomId);
|
|
669
541
|
}
|
|
670
|
-
/**
|
|
671
|
-
*
|
|
542
|
+
/** The backing collection, bound to the CURRENT facade. Recreated when
|
|
543
|
+
* the facade was dropped (post-`chat.disconnect()`) — the old one would
|
|
544
|
+
* never receive dispatches again. */
|
|
545
|
+
collection() {
|
|
546
|
+
const facade = this.room;
|
|
547
|
+
if (this.col && this.col.room === facade) return this.col;
|
|
548
|
+
this.colUnsub?.();
|
|
549
|
+
this.col = facade.collection();
|
|
550
|
+
this.colUnsub = this.col.on("change", () => this.rebuild());
|
|
551
|
+
this.rebuild();
|
|
552
|
+
return this.col;
|
|
553
|
+
}
|
|
554
|
+
/** Subscribe page lifecycle + ensure the collection is live. Safe to call
|
|
555
|
+
* repeatedly (no-op while attached) — matches React 18/19 StrictMode's
|
|
672
556
|
* mount → cleanup → mount effect cycle. */
|
|
673
557
|
attach() {
|
|
674
558
|
if (this.attached) return;
|
|
675
559
|
this.attached = true;
|
|
676
|
-
|
|
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
|
-
);
|
|
560
|
+
this.collection();
|
|
686
561
|
if (typeof document !== "undefined") {
|
|
687
562
|
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
688
563
|
}
|
|
@@ -690,13 +565,13 @@ var RoomStore = class {
|
|
|
690
565
|
window.addEventListener("pagehide", this.onPageHide);
|
|
691
566
|
}
|
|
692
567
|
}
|
|
693
|
-
/**
|
|
694
|
-
*
|
|
568
|
+
/** Detach page listeners and flush the pending read watermark. The
|
|
569
|
+
* collection (and its state) survives, so a later `attach()` resumes
|
|
570
|
+
* cleanly — StrictMode's mount → cleanup → mount cycle keeps messages. */
|
|
695
571
|
dispose() {
|
|
696
572
|
if (!this.attached) return;
|
|
697
573
|
this.attached = false;
|
|
698
574
|
this.room.flushMarkRead();
|
|
699
|
-
for (const u of this.unsubs.splice(0)) u();
|
|
700
575
|
if (typeof document !== "undefined") {
|
|
701
576
|
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
702
577
|
}
|
|
@@ -711,59 +586,42 @@ var RoomStore = class {
|
|
|
711
586
|
return () => this.listeners.delete(fn);
|
|
712
587
|
};
|
|
713
588
|
getSnapshot = () => this.snapshot;
|
|
714
|
-
|
|
589
|
+
rebuild() {
|
|
590
|
+
const col = this.col;
|
|
591
|
+
if (!col) return;
|
|
715
592
|
this.snapshot = {
|
|
716
|
-
messages: [...
|
|
717
|
-
hasMore:
|
|
718
|
-
readWatermarks: { ...
|
|
593
|
+
messages: [...col.messages],
|
|
594
|
+
hasMore: col.hasMore,
|
|
595
|
+
readWatermarks: { ...col.readWatermarks }
|
|
719
596
|
};
|
|
720
597
|
for (const fn of this.listeners) fn();
|
|
721
598
|
}
|
|
722
599
|
// ── read receipts ───────────────────────────────────────────────
|
|
723
600
|
/** Whether `userId` has read `messageId` according to the latest watermark. */
|
|
724
601
|
isReadBy(userId, messageId) {
|
|
725
|
-
|
|
726
|
-
if (!w) return false;
|
|
727
|
-
return !idGreater(messageId, w);
|
|
602
|
+
return this.collection().isReadBy(userId, messageId);
|
|
728
603
|
}
|
|
729
604
|
/** How many of the given users have read `messageId`. Pass the room's
|
|
730
605
|
* member ids (minus the sender) to get a KakaoTalk-style unread count:
|
|
731
606
|
* `members.length - readCount(...)`. */
|
|
732
607
|
readCount(messageId, userIds) {
|
|
733
|
-
|
|
734
|
-
for (const u of userIds) if (this.isReadBy(u, messageId)) n++;
|
|
735
|
-
return n;
|
|
608
|
+
return this.collection().readCount(messageId, userIds);
|
|
736
609
|
}
|
|
737
610
|
// ── history ─────────────────────────────────────────────────────
|
|
738
|
-
/** Load the most recent page of history
|
|
739
|
-
*
|
|
740
|
-
*
|
|
611
|
+
/** Load the most recent page of history. No-op after the first call
|
|
612
|
+
* (StrictMode double-mount safe) — use `loadMore` for scrollback.
|
|
613
|
+
* 캐시(`ClientOptions.cacheStore`)가 켜져 있으면 스냅샷 즉시 렌더 →
|
|
614
|
+
* API 도착 시 통째 교체가 컬렉션 안에서 일어난다. */
|
|
741
615
|
async loadHistory(limit = 50) {
|
|
742
616
|
if (this.historyRequested) return;
|
|
743
617
|
this.historyRequested = true;
|
|
744
|
-
|
|
745
|
-
this.messages.unshift(...page.items.map(chatMessageFromHistory));
|
|
746
|
-
this.oldestCursor = page.nextCursor;
|
|
747
|
-
this.hasMore = page.hasMore;
|
|
748
|
-
this.emit();
|
|
618
|
+
await this.collection().load(limit);
|
|
749
619
|
}
|
|
750
620
|
/** Scrollback: load the page of messages older than what's on screen and
|
|
751
621
|
* prepend it. No-op while a previous call is in flight or when `hasMore`
|
|
752
622
|
* is false. Returns the number of messages added. */
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
}
|
|
623
|
+
loadMore(limit = 50) {
|
|
624
|
+
return this.collection().loadMore(limit);
|
|
767
625
|
}
|
|
768
626
|
// ── outgoing ────────────────────────────────────────────────────
|
|
769
627
|
/** Send `text` optimistically: a `sending` bubble appears immediately,
|
|
@@ -772,221 +630,45 @@ var RoomStore = class {
|
|
|
772
630
|
* Optional extras ride along: `replyToId` for replies, `customType` /
|
|
773
631
|
* `customMeta` for app-defined conventions (e.g. message priority). */
|
|
774
632
|
send(text, opts) {
|
|
775
|
-
|
|
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
|
-
})();
|
|
633
|
+
this.collection().send(text, opts);
|
|
802
634
|
}
|
|
803
635
|
/** Send a file optimistically. A FILE bubble with `uploadProgress`
|
|
804
636
|
* appears immediately; progress advances during the S3 upload, then the
|
|
805
637
|
* bubble flips to `sent` on ack (or `failed`). */
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
}
|
|
638
|
+
sendFile(file, opts) {
|
|
639
|
+
return this.collection().sendFile(file, opts);
|
|
847
640
|
}
|
|
848
641
|
/** Edit a message's content (server-side; peers get `message_updated`).
|
|
849
642
|
* The local copy updates optimistically. */
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
this.mutateById(messageId, (m) => ({
|
|
853
|
-
...m,
|
|
854
|
-
content,
|
|
855
|
-
editedCount: m.editedCount + 1
|
|
856
|
-
}));
|
|
643
|
+
edit(messageId, content) {
|
|
644
|
+
return this.collection().edit(messageId, content);
|
|
857
645
|
}
|
|
858
646
|
/** Delete a message. `scope: "ALL"` (default) removes it for everyone;
|
|
859
647
|
* `"MY"` hides it only for the caller. Local copy flips to deleted. */
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
this.mutateById(messageId, (m) => ({ ...m, isDeleted: true }));
|
|
648
|
+
delete(messageId, scope = "ALL") {
|
|
649
|
+
return this.collection().delete(messageId, scope);
|
|
863
650
|
}
|
|
864
651
|
/** Add/remove the caller's reaction. Pass `myUserId` so the toggle can
|
|
865
652
|
* tell whether the caller already reacted with `key`. The authoritative
|
|
866
653
|
* state arrives back via the reaction WS events. */
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
}
|
|
654
|
+
toggleReaction(messageId, key, myUserId) {
|
|
655
|
+
return this.collection().toggleReaction(messageId, key, myUserId);
|
|
877
656
|
}
|
|
878
657
|
/** Search this room's messages (server-side, content substring match).
|
|
879
658
|
* Returns view models without touching the message list. */
|
|
880
659
|
async search(q, limit = 30) {
|
|
881
660
|
const page = await this.room.search(q, { limit });
|
|
882
|
-
return page.items.map(chatMessageFromHistory);
|
|
661
|
+
return page.items.map(import_sdk_web.chatMessageFromHistory);
|
|
883
662
|
}
|
|
884
663
|
/** Mark `messageId` read (debounced inside the SDK). */
|
|
885
664
|
markRead(messageId) {
|
|
886
|
-
this.
|
|
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();
|
|
665
|
+
this.collection().markRead(messageId);
|
|
960
666
|
}
|
|
961
667
|
onVisibilityChange = () => {
|
|
962
668
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
963
669
|
this.room.flushMarkRead();
|
|
964
670
|
}
|
|
965
671
|
};
|
|
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
672
|
};
|
|
991
673
|
|
|
992
674
|
// src/use-messages.ts
|