@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/LICENSE +21 -0
- package/README.ko.md +99 -0
- package/README.md +99 -0
- package/dist/index.cjs +1192 -0
- package/dist/index.d.cts +675 -0
- package/dist/index.d.ts +675 -0
- package/dist/index.js +1154 -0
- package/package.json +42 -0
- package/src/chat-message.ts +209 -0
- package/src/context.tsx +72 -0
- package/src/index.ts +83 -0
- package/src/internal/compare-id.ts +17 -0
- package/src/member-list-store.ts +116 -0
- package/src/room-files-store.ts +130 -0
- package/src/room-list-store.ts +227 -0
- package/src/room-settings-store.ts +200 -0
- package/src/room-store.ts +475 -0
- package/src/use-member-list.ts +46 -0
- package/src/use-messages.ts +62 -0
- package/src/use-room-files.ts +53 -0
- package/src/use-room-list.ts +57 -0
- package/src/use-room-settings.ts +47 -0
- package/src/use-room.ts +21 -0
- package/src/use-typing.ts +85 -0
- package/src/use-unread.ts +65 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { SenderMini, FileInline, ReactionEntry, MessageOut, WsChatReceive, ClientOptions, NoveraChat, RoomMemberOut, FileMeta, MemberPreview, InviteToken, JoinRequestOut, PushTrigger, SendFileOptions, DeleteScope, Room, UnreadSummary } from '@noverachat/sdk-web';
|
|
2
|
+
export * from '@noverachat/sdk-web';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/** Delivery status of an outgoing (optimistic) message. */
|
|
6
|
+
type ChatMessageStatus = "sending" | "sent" | "failed";
|
|
7
|
+
/**
|
|
8
|
+
* A single message as the UI wants it — one uniform shape over the SDK's
|
|
9
|
+
* three sources: `MessageOut` (history/REST), `WsChatReceive` (live/WS), and
|
|
10
|
+
* an optimistic bubble we just sent.
|
|
11
|
+
*
|
|
12
|
+
* Treat instances as immutable — `RoomStore` evolves them by replacement,
|
|
13
|
+
* never mutation.
|
|
14
|
+
*/
|
|
15
|
+
interface ChatMessage {
|
|
16
|
+
/** The server message id, or the `tempId` while still `sending`. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Author user id. `null` means the current user (an optimistic bubble). */
|
|
19
|
+
senderId: string | null;
|
|
20
|
+
content: string | null;
|
|
21
|
+
/** Creation time in unix milliseconds. */
|
|
22
|
+
createdAtMs: number | null;
|
|
23
|
+
status: ChatMessageStatus;
|
|
24
|
+
/** `TEXT | FILE | EMOTICON | ADMIN | SYSTEM` — UIs use this to render
|
|
25
|
+
* system/admin lines (centered notices) differently from chat bubbles. */
|
|
26
|
+
messageType: string;
|
|
27
|
+
/** Sender identity (nickname / profile image) when the server included it.
|
|
28
|
+
* Null for optimistic bubbles and live frames (which carry only
|
|
29
|
+
* `senderId`) — UIs typically resolve the profile from their member list. */
|
|
30
|
+
sender: SenderMini | null;
|
|
31
|
+
/** App-defined subtype (e.g. a message-priority convention). */
|
|
32
|
+
customType: string | null;
|
|
33
|
+
/** Snowflake id (string) of the attached file, when this is a file message. */
|
|
34
|
+
fileId: string | null;
|
|
35
|
+
/** Inline metadata of the attached file — enough to render a placeholder
|
|
36
|
+
* without a round-trip. May be null even when `fileId` is set; fetch via
|
|
37
|
+
* `chat.getFileMeta` then. */
|
|
38
|
+
file: FileInline | null;
|
|
39
|
+
/** Multi-file bundle (`file_group`). Null for single-file / text messages.
|
|
40
|
+
* Check `files?.length` first and fall back to the `file` singleton. */
|
|
41
|
+
files: FileInline[] | null;
|
|
42
|
+
/** Id of the message this one replies to, if any. */
|
|
43
|
+
replyToId: string | null;
|
|
44
|
+
/** Emoji reactions grouped by key. Kept live by `RoomStore` from the
|
|
45
|
+
* reaction WS events. */
|
|
46
|
+
reactions: ReactionEntry[];
|
|
47
|
+
/** Sender-attached custom metadata (`data._customMeta`). */
|
|
48
|
+
customMeta: Record<string, unknown> | null;
|
|
49
|
+
/** How many times the message was edited. `> 0` → show an "edited" mark. */
|
|
50
|
+
editedCount: number;
|
|
51
|
+
/** Upload progress in [0, 1] while an optimistic FILE message's bytes are
|
|
52
|
+
* still going up to S3. Null once committed (or for non-file messages). */
|
|
53
|
+
uploadProgress: number | null;
|
|
54
|
+
/** The optimistic temp id, kept for ack reconciliation. Null once confirmed. */
|
|
55
|
+
tempId: string | null;
|
|
56
|
+
isDeleted: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Build a `ChatMessage` from server history (REST). */
|
|
59
|
+
declare function chatMessageFromHistory(m: MessageOut): ChatMessage;
|
|
60
|
+
/** Build a `ChatMessage` from a live inbound frame (WS). */
|
|
61
|
+
declare function chatMessageFromLive(f: WsChatReceive): ChatMessage;
|
|
62
|
+
|
|
63
|
+
interface NoveraChatProviderProps {
|
|
64
|
+
/** Options for the `NoveraChat` this provider creates. Read **once**, when
|
|
65
|
+
* the provider first mounts — to connect with different options, give the
|
|
66
|
+
* provider a new `key` so it remounts. */
|
|
67
|
+
options: ClientOptions;
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Provides a connected `NoveraChat` to the component subtree.
|
|
72
|
+
*
|
|
73
|
+
* Mount it once near the top of your app. It creates the client, calls
|
|
74
|
+
* `connect()` on mount and `disconnect()` on unmount. Descendants read the
|
|
75
|
+
* client with `useNoveraChat()`.
|
|
76
|
+
*
|
|
77
|
+
* ```tsx
|
|
78
|
+
* <NoveraChatProvider
|
|
79
|
+
* options={{
|
|
80
|
+
* appId: "app_9f8k2x",
|
|
81
|
+
* endpoint: "https://chat.example.com",
|
|
82
|
+
* tokenProvider: async () => fetchJwt(),
|
|
83
|
+
* }}
|
|
84
|
+
* >
|
|
85
|
+
* <ChatScreen />
|
|
86
|
+
* </NoveraChatProvider>
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function NoveraChatProvider(props: NoveraChatProviderProps): ReactNode;
|
|
90
|
+
/**
|
|
91
|
+
* The `NoveraChat` from the nearest enclosing `NoveraChatProvider`.
|
|
92
|
+
* Throws when there is no provider above in the tree.
|
|
93
|
+
*
|
|
94
|
+
* ```tsx
|
|
95
|
+
* const chat = useNoveraChat();
|
|
96
|
+
* const room = chat.room("room_123");
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function useNoveraChat(): NoveraChat;
|
|
100
|
+
|
|
101
|
+
interface MemberListSnapshot {
|
|
102
|
+
/** Current members (server order). Empty until `load` completes. */
|
|
103
|
+
members: RoomMemberOut[];
|
|
104
|
+
/** Members with the OPERATOR role — e.g. to show a crown badge or gate
|
|
105
|
+
* operator-only actions client-side. */
|
|
106
|
+
operators: RoomMemberOut[];
|
|
107
|
+
/** Active (non-KICKED) member ids — handy as the `userIds` argument of
|
|
108
|
+
* `RoomStore.readCount` for read-receipt math. */
|
|
109
|
+
activeMemberIds: string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Live member list of one room — roles, presence dots, mute state. Backs a
|
|
113
|
+
* room-settings member section or an operator panel.
|
|
114
|
+
*
|
|
115
|
+
* Loads via `room.listMembers()` and keeps itself fresh:
|
|
116
|
+
* - membership / lifecycle `roomEvent`s for this room trigger a
|
|
117
|
+
* debounced reload (member joined / left / kicked / role changed /
|
|
118
|
+
* muted …);
|
|
119
|
+
* - `presence` transitions flip the member's `isOnline` in place, no
|
|
120
|
+
* round-trip.
|
|
121
|
+
*/
|
|
122
|
+
declare class MemberListStore {
|
|
123
|
+
private readonly chat;
|
|
124
|
+
readonly roomId: string;
|
|
125
|
+
private readonly listeners;
|
|
126
|
+
private members;
|
|
127
|
+
private unsubs;
|
|
128
|
+
private reloadTimer;
|
|
129
|
+
private snapshot;
|
|
130
|
+
private attached;
|
|
131
|
+
constructor(chat: NoveraChat, roomId: string);
|
|
132
|
+
attach(): void;
|
|
133
|
+
dispose(): void;
|
|
134
|
+
readonly subscribe: (fn: () => void) => (() => void);
|
|
135
|
+
readonly getSnapshot: () => MemberListSnapshot;
|
|
136
|
+
private emit;
|
|
137
|
+
/** Whether `userId` is an OPERATOR of this room. */
|
|
138
|
+
isOperator(userId: string): boolean;
|
|
139
|
+
/** Fetch the member list. Also called automatically on membership
|
|
140
|
+
* events. */
|
|
141
|
+
load(): Promise<void>;
|
|
142
|
+
private onPresence;
|
|
143
|
+
private scheduleReload;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One (message, file) entry of a room's shared-file list — the same shape
|
|
147
|
+
* `room.listFiles()` returns per item. */
|
|
148
|
+
interface RoomFileItem {
|
|
149
|
+
messageId: string;
|
|
150
|
+
senderId: string | null;
|
|
151
|
+
createdAtMs: number;
|
|
152
|
+
file: FileMeta;
|
|
153
|
+
}
|
|
154
|
+
interface RoomFilesSnapshot {
|
|
155
|
+
/** Loaded entries, newest first. One entry per (message, file). */
|
|
156
|
+
items: RoomFileItem[];
|
|
157
|
+
/** Only image/video entries — for a photo-album grid that skips plain
|
|
158
|
+
* files. */
|
|
159
|
+
media: RoomFileItem[];
|
|
160
|
+
/** True when older pages remain — call `loadMore` from the grid's tail. */
|
|
161
|
+
hasMore: boolean;
|
|
162
|
+
/** True while a `refresh` / `loadMore` request is in flight. */
|
|
163
|
+
isLoading: boolean;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Paginated media/file grid of one room — backs a "파일" section in room
|
|
167
|
+
* settings or an album-style photo grid.
|
|
168
|
+
*
|
|
169
|
+
* Wraps `room.listFiles()` (newest first) with cursor bookkeeping. New
|
|
170
|
+
* uploads land at the head on `refresh`; older pages append via `loadMore`.
|
|
171
|
+
* Render thumbnails with `chat.fileThumbnailUrl(item.file.id, token)`.
|
|
172
|
+
*/
|
|
173
|
+
declare class RoomFilesStore {
|
|
174
|
+
private readonly chat;
|
|
175
|
+
readonly roomId: string;
|
|
176
|
+
/** Page size for both `refresh` and `loadMore`. */
|
|
177
|
+
readonly pageSize: number;
|
|
178
|
+
private readonly listeners;
|
|
179
|
+
private items;
|
|
180
|
+
private cursor;
|
|
181
|
+
private hasMore;
|
|
182
|
+
private loading;
|
|
183
|
+
private attached;
|
|
184
|
+
private snapshot;
|
|
185
|
+
constructor(chat: NoveraChat, roomId: string, pageSize?: number);
|
|
186
|
+
/** No realtime subscriptions — kept for lifecycle symmetry with the
|
|
187
|
+
* other stores (guards in-flight results after unmount). */
|
|
188
|
+
attach(): void;
|
|
189
|
+
dispose(): void;
|
|
190
|
+
readonly subscribe: (fn: () => void) => (() => void);
|
|
191
|
+
readonly getSnapshot: () => RoomFilesSnapshot;
|
|
192
|
+
private emit;
|
|
193
|
+
/** Reload from the newest page (clears what was loaded). */
|
|
194
|
+
refresh(): Promise<void>;
|
|
195
|
+
/** Fetch the next older page and append it. No-op while loading or when
|
|
196
|
+
* `hasMore` is false. Returns the number of entries added. */
|
|
197
|
+
loadMore(): Promise<number>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One row of a chat-room list screen.
|
|
202
|
+
*
|
|
203
|
+
* A live-updatable view over `UnreadRoomItem`: same fields, plus
|
|
204
|
+
* `lastMessageAtMs` which is filled from realtime messages as they arrive
|
|
205
|
+
* (the initial REST load doesn't carry a timestamp yet) and
|
|
206
|
+
* `anyMemberOnline` for the green presence dot.
|
|
207
|
+
*/
|
|
208
|
+
interface RoomListItem {
|
|
209
|
+
roomId: string;
|
|
210
|
+
name: string | null;
|
|
211
|
+
roomType: string | null;
|
|
212
|
+
unreadCount: number;
|
|
213
|
+
lastMessageId: string | null;
|
|
214
|
+
lastMessagePreview: string | null;
|
|
215
|
+
memberCount: number;
|
|
216
|
+
members: MemberPreview[];
|
|
217
|
+
/** unix ms of the last message — filled from realtime frames onward. */
|
|
218
|
+
lastMessageAtMs: number | null;
|
|
219
|
+
/** Green dot — true when any preview member is online (as of the last
|
|
220
|
+
* list load). */
|
|
221
|
+
anyMemberOnline: boolean;
|
|
222
|
+
}
|
|
223
|
+
interface RoomListSnapshot {
|
|
224
|
+
/** Rooms, most-recently-active first. */
|
|
225
|
+
rooms: RoomListItem[];
|
|
226
|
+
/** Sum of every room's unread badge (for a bottom-tab badge). */
|
|
227
|
+
totalUnread: number;
|
|
228
|
+
}
|
|
229
|
+
interface RoomListStoreOptions {
|
|
230
|
+
/** Incoming message → list preview string. Default: `content`, or "📎"
|
|
231
|
+
* for file messages. */
|
|
232
|
+
previewBuilder?: (m: WsChatReceive) => string;
|
|
233
|
+
/** Return true while the user is looking at the given room, so its new
|
|
234
|
+
* messages don't bump the badge (the room screen's `markRead` owns the
|
|
235
|
+
* read state then). */
|
|
236
|
+
isRoomVisible?: (roomId: string) => boolean;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Chat-room list state — the data behind a KakaoTalk-style "채팅" tab.
|
|
240
|
+
*
|
|
241
|
+
* Loads the room list from `chat.unreadSummary()` (rooms come back
|
|
242
|
+
* most-recently-active first) and keeps it live:
|
|
243
|
+
* - a new message in any room bumps that room to the top, replaces its
|
|
244
|
+
* preview, and (unless `isRoomVisible` says the user is looking at it)
|
|
245
|
+
* increments its unread badge;
|
|
246
|
+
* - room membership events and cross-device read syncs trigger a
|
|
247
|
+
* debounced reload.
|
|
248
|
+
*
|
|
249
|
+
* When coming back from a room screen, call `markRead(roomId)` to clear
|
|
250
|
+
* the badge. Previews of the user's own sends are picked up by the reload
|
|
251
|
+
* on return (`load()` is idempotent-safe to call again).
|
|
252
|
+
*/
|
|
253
|
+
declare class RoomListStore {
|
|
254
|
+
private readonly chat;
|
|
255
|
+
private readonly opts;
|
|
256
|
+
private readonly listeners;
|
|
257
|
+
private rooms;
|
|
258
|
+
private index;
|
|
259
|
+
private roomSubs;
|
|
260
|
+
private clientSub;
|
|
261
|
+
private reloadTimer;
|
|
262
|
+
private snapshot;
|
|
263
|
+
private attached;
|
|
264
|
+
constructor(chat: NoveraChat, opts?: RoomListStoreOptions);
|
|
265
|
+
/** Subscribe to client-level room events. Idempotent. */
|
|
266
|
+
attach(): void;
|
|
267
|
+
/** Unsubscribe everything (list state is kept for a later `attach`). */
|
|
268
|
+
dispose(): void;
|
|
269
|
+
readonly subscribe: (fn: () => void) => (() => void);
|
|
270
|
+
readonly getSnapshot: () => RoomListSnapshot;
|
|
271
|
+
private emit;
|
|
272
|
+
/** Reload the room list and re-hook the realtime subscriptions. */
|
|
273
|
+
load(): Promise<void>;
|
|
274
|
+
/** Hide a room (카톡 "숨기기") — optimistically remove it from the list
|
|
275
|
+
* and send `hide` to the server. On failure the next `load` brings it
|
|
276
|
+
* back; new activity may re-surface it per server policy (arrives via
|
|
277
|
+
* roomEvent → reload). */
|
|
278
|
+
hideRoom(roomId: string): Promise<void>;
|
|
279
|
+
/** Undo `hideRoom` — send `unhide` and reload the list. */
|
|
280
|
+
unhideRoom(roomId: string): Promise<void>;
|
|
281
|
+
/** Mark a room read — drop its badge to 0 and advance the client's
|
|
282
|
+
* last-seen watermark. */
|
|
283
|
+
markRead(roomId: string): void;
|
|
284
|
+
private reindex;
|
|
285
|
+
private resubscribe;
|
|
286
|
+
private onIncoming;
|
|
287
|
+
private scheduleReload;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface RoomSettingsSnapshot {
|
|
291
|
+
/** Invite tokens minted for this room (operator view), newest first.
|
|
292
|
+
* Empty until `loadInvites`. */
|
|
293
|
+
invites: InviteToken[];
|
|
294
|
+
/** Pending join requests (operator inbox). Empty until
|
|
295
|
+
* `loadJoinRequests`. */
|
|
296
|
+
joinRequests: JoinRequestOut[];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Actions and state behind a room-settings screen (대화방 설정): per-room
|
|
300
|
+
* mute, invite links, join-request inbox, leave / clear-history, and
|
|
301
|
+
* operator moderation.
|
|
302
|
+
*
|
|
303
|
+
* Mostly a thin, typed facade over `Room`'s one-shot REST calls — the only
|
|
304
|
+
* held state is the invite / join-request lists. Read the caller's
|
|
305
|
+
* *current* push trigger from their own row in `MemberListStore` (the
|
|
306
|
+
* server has no standalone "get my settings" endpoint).
|
|
307
|
+
*/
|
|
308
|
+
declare class RoomSettingsStore {
|
|
309
|
+
private readonly chat;
|
|
310
|
+
readonly roomId: string;
|
|
311
|
+
private readonly listeners;
|
|
312
|
+
private invites;
|
|
313
|
+
private joinRequests;
|
|
314
|
+
private attached;
|
|
315
|
+
private snapshot;
|
|
316
|
+
constructor(chat: NoveraChat, roomId: string);
|
|
317
|
+
private get room();
|
|
318
|
+
/** No realtime subscriptions — kept for lifecycle symmetry with the
|
|
319
|
+
* other stores (guards in-flight results after unmount). */
|
|
320
|
+
attach(): void;
|
|
321
|
+
dispose(): void;
|
|
322
|
+
readonly subscribe: (fn: () => void) => (() => void);
|
|
323
|
+
readonly getSnapshot: () => RoomSettingsSnapshot;
|
|
324
|
+
private emit;
|
|
325
|
+
/** Convenience mute toggle: `true` → `OFF`, `false` → `ALL`. */
|
|
326
|
+
setMuted(muted: boolean): Promise<void>;
|
|
327
|
+
/** Fine-grained per-room push trigger: `ALL` | `MENTION_ONLY` | `OFF`. */
|
|
328
|
+
setPushTrigger(trigger: PushTrigger): Promise<void>;
|
|
329
|
+
/** Fetch the invite list (operator-only server-side). */
|
|
330
|
+
loadInvites(): Promise<void>;
|
|
331
|
+
/** Mint a new invite link and prepend it to `invites`. */
|
|
332
|
+
createInvite(opts?: {
|
|
333
|
+
expiresInHours?: number | null;
|
|
334
|
+
maxUses?: number | null;
|
|
335
|
+
}): Promise<InviteToken>;
|
|
336
|
+
/** Revoke a token and drop it from `invites`. */
|
|
337
|
+
revokeInvite(token: string): Promise<void>;
|
|
338
|
+
/** Fetch pending join requests (operator-only server-side). */
|
|
339
|
+
loadJoinRequests(): Promise<void>;
|
|
340
|
+
/** Approve a pending request and drop it from `joinRequests`. */
|
|
341
|
+
approveJoinRequest(userId: string, reason?: string): Promise<void>;
|
|
342
|
+
/** Reject a pending request and drop it from `joinRequests`. */
|
|
343
|
+
rejectJoinRequest(userId: string, reason?: string): Promise<void>;
|
|
344
|
+
/** Per-user "clear chat" — hides existing messages for the caller only. */
|
|
345
|
+
clearHistory(): Promise<void>;
|
|
346
|
+
/** Leave the room. `deleteMessages`: `"none"` (default) | `"mine"` |
|
|
347
|
+
* `"all"` — see `Room.leave`. */
|
|
348
|
+
leave(options?: {
|
|
349
|
+
deleteMessages?: "none" | "mine" | "all";
|
|
350
|
+
}): Promise<void>;
|
|
351
|
+
/** Delete every message the caller sent (without leaving). Returns the
|
|
352
|
+
* cleared count. */
|
|
353
|
+
deleteMyMessages(): Promise<number>;
|
|
354
|
+
/** **Destructive, operator-only** — wipe every message in the room. */
|
|
355
|
+
deleteAllMessages(): Promise<{
|
|
356
|
+
cleared: number;
|
|
357
|
+
upToMessageId: string | null;
|
|
358
|
+
}>;
|
|
359
|
+
/** Operator: block non-operator sends (`ROOM_FROZEN` server-side). */
|
|
360
|
+
freeze(): Promise<void>;
|
|
361
|
+
/** Operator: reopen the room to sends. */
|
|
362
|
+
unfreeze(): Promise<void>;
|
|
363
|
+
/** Operator: toggle open-chat visibility. */
|
|
364
|
+
setPublic(isPublic: boolean): Promise<void>;
|
|
365
|
+
/** Operator: mute a member (`durationMinutes` null → indefinite). */
|
|
366
|
+
muteMember(userId: string, durationMinutes: number | null): Promise<void>;
|
|
367
|
+
/** Operator: clear a member's mute. */
|
|
368
|
+
unmuteMember(userId: string): Promise<void>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Immutable snapshot handed to React via `useSyncExternalStore`. A new
|
|
372
|
+
* object (new array/record identities) is produced on every change. */
|
|
373
|
+
interface RoomSnapshot {
|
|
374
|
+
/** The current messages, oldest first. */
|
|
375
|
+
messages: ChatMessage[];
|
|
376
|
+
/** True when older history remains — show a "load more" affordance. */
|
|
377
|
+
hasMore: boolean;
|
|
378
|
+
/** Per-user read watermarks (`userId` → last read `messageId`), kept live
|
|
379
|
+
* from `sync_tick` frames. */
|
|
380
|
+
readWatermarks: Readonly<Record<string, string>>;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Turns a `Room`'s events into an externally-subscribable message list and
|
|
384
|
+
* drives optimistic sends. Framework-agnostic on purpose so it can also be
|
|
385
|
+
* used outside hooks (tests, non-React glue).
|
|
386
|
+
*
|
|
387
|
+
* Lifecycle: construct → `attach()` (subscribe WS events; idempotent) →
|
|
388
|
+
* `dispose()` (flush read watermark + unsubscribe). `useMessages` does all
|
|
389
|
+
* three for you.
|
|
390
|
+
*
|
|
391
|
+
* Holds `(chat, roomId)` rather than a `Room` instance and re-resolves the
|
|
392
|
+
* live facade on every use — `chat.disconnect()` drops its Room facades, so
|
|
393
|
+
* a remounted provider (React StrictMode simulates this) would otherwise
|
|
394
|
+
* leave the store subscribed to an orphaned Room that no longer receives
|
|
395
|
+
* dispatches.
|
|
396
|
+
*
|
|
397
|
+
* ```ts
|
|
398
|
+
* const store = new RoomStore(chat, "room_123");
|
|
399
|
+
* store.attach();
|
|
400
|
+
* const unsub = store.subscribe(() => render(store.getSnapshot()));
|
|
401
|
+
* await store.loadHistory();
|
|
402
|
+
* store.send("hello");
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
declare class RoomStore {
|
|
406
|
+
private readonly chat;
|
|
407
|
+
readonly roomId: string;
|
|
408
|
+
private readonly listeners;
|
|
409
|
+
private readonly unsubs;
|
|
410
|
+
private messages;
|
|
411
|
+
private readWatermarks;
|
|
412
|
+
private hasMore;
|
|
413
|
+
private oldestCursor;
|
|
414
|
+
private loadingMore;
|
|
415
|
+
private historyRequested;
|
|
416
|
+
private snapshot;
|
|
417
|
+
private attached;
|
|
418
|
+
private readonly onPageHide;
|
|
419
|
+
constructor(chat: NoveraChat, roomId: string);
|
|
420
|
+
/** The live `Room` facade — resolved from the client on every access
|
|
421
|
+
* (get-or-create in a map, cheap) so we never act on an orphan. */
|
|
422
|
+
private get room();
|
|
423
|
+
/** Subscribe to WS events + page lifecycle. Safe to call repeatedly
|
|
424
|
+
* (no-op while attached) — matches React 18/19 StrictMode's
|
|
425
|
+
* mount → cleanup → mount effect cycle. */
|
|
426
|
+
attach(): void;
|
|
427
|
+
/** Unsubscribe everything and flush the pending read watermark. The store
|
|
428
|
+
* keeps its message state, so a later `attach()` resumes cleanly. */
|
|
429
|
+
dispose(): void;
|
|
430
|
+
/** `useSyncExternalStore`-compatible subscribe. Returns an unsubscribe fn. */
|
|
431
|
+
readonly subscribe: (fn: () => void) => (() => void);
|
|
432
|
+
readonly getSnapshot: () => RoomSnapshot;
|
|
433
|
+
private emit;
|
|
434
|
+
/** Whether `userId` has read `messageId` according to the latest watermark. */
|
|
435
|
+
isReadBy(userId: string, messageId: string): boolean;
|
|
436
|
+
/** How many of the given users have read `messageId`. Pass the room's
|
|
437
|
+
* member ids (minus the sender) to get a KakaoTalk-style unread count:
|
|
438
|
+
* `members.length - readCount(...)`. */
|
|
439
|
+
readCount(messageId: string, userIds: Iterable<string>): number;
|
|
440
|
+
/** Load the most recent page of history and prepend it. No-op after the
|
|
441
|
+
* first call (StrictMode double-mount safe) — use `loadMore` for
|
|
442
|
+
* scrollback. */
|
|
443
|
+
loadHistory(limit?: number): Promise<void>;
|
|
444
|
+
/** Scrollback: load the page of messages older than what's on screen and
|
|
445
|
+
* prepend it. No-op while a previous call is in flight or when `hasMore`
|
|
446
|
+
* is false. Returns the number of messages added. */
|
|
447
|
+
loadMore(limit?: number): Promise<number>;
|
|
448
|
+
/** Send `text` optimistically: a `sending` bubble appears immediately,
|
|
449
|
+
* then flips to `sent` (with the real id) on ack, or `failed` on error.
|
|
450
|
+
*
|
|
451
|
+
* Optional extras ride along: `replyToId` for replies, `customType` /
|
|
452
|
+
* `customMeta` for app-defined conventions (e.g. message priority). */
|
|
453
|
+
send(text: string, opts?: {
|
|
454
|
+
replyToId?: string;
|
|
455
|
+
customType?: string;
|
|
456
|
+
customMeta?: Record<string, unknown>;
|
|
457
|
+
}): void;
|
|
458
|
+
/** Send a file optimistically. A FILE bubble with `uploadProgress`
|
|
459
|
+
* appears immediately; progress advances during the S3 upload, then the
|
|
460
|
+
* bubble flips to `sent` on ack (or `failed`). */
|
|
461
|
+
sendFile(file: File, opts?: SendFileOptions): Promise<void>;
|
|
462
|
+
/** Edit a message's content (server-side; peers get `message_updated`).
|
|
463
|
+
* The local copy updates optimistically. */
|
|
464
|
+
edit(messageId: string, content: string): Promise<void>;
|
|
465
|
+
/** Delete a message. `scope: "ALL"` (default) removes it for everyone;
|
|
466
|
+
* `"MY"` hides it only for the caller. Local copy flips to deleted. */
|
|
467
|
+
delete(messageId: string, scope?: DeleteScope): Promise<void>;
|
|
468
|
+
/** Add/remove the caller's reaction. Pass `myUserId` so the toggle can
|
|
469
|
+
* tell whether the caller already reacted with `key`. The authoritative
|
|
470
|
+
* state arrives back via the reaction WS events. */
|
|
471
|
+
toggleReaction(messageId: string, key: string, myUserId: string): Promise<void>;
|
|
472
|
+
/** Search this room's messages (server-side, content substring match).
|
|
473
|
+
* Returns view models without touching the message list. */
|
|
474
|
+
search(q: string, limit?: number): Promise<ChatMessage[]>;
|
|
475
|
+
/** Mark `messageId` read (debounced inside the SDK). */
|
|
476
|
+
markRead(messageId: string): void;
|
|
477
|
+
private onIncoming;
|
|
478
|
+
private onUpdated;
|
|
479
|
+
private onDeleted;
|
|
480
|
+
private onCleared;
|
|
481
|
+
private onReactionAdded;
|
|
482
|
+
private onReactionRemoved;
|
|
483
|
+
private onSyncTick;
|
|
484
|
+
private onVisibilityChange;
|
|
485
|
+
private bindAck;
|
|
486
|
+
private mutateById;
|
|
487
|
+
private mutateByTempId;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
interface UseMessagesOptions {
|
|
491
|
+
/** Page size for the initial history load (and a sensible default for
|
|
492
|
+
* scrollback pages). Default 50. */
|
|
493
|
+
historyLimit?: number;
|
|
494
|
+
}
|
|
495
|
+
interface UseMessagesResult extends RoomSnapshot {
|
|
496
|
+
/** The backing store — exposes `send`, `sendFile`, `edit`, `delete`,
|
|
497
|
+
* `toggleReaction`, `markRead`, `loadMore`, `search`, `isReadBy`,
|
|
498
|
+
* `readCount`. Stable identity for the lifetime of `(client, roomId)`. */
|
|
499
|
+
store: RoomStore;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The live message list of a room, plus the store that drives it.
|
|
503
|
+
*
|
|
504
|
+
* Subscribes to the room's WS events, loads the first history page on
|
|
505
|
+
* mount, and re-renders on every change — new/edited/deleted messages,
|
|
506
|
+
* reactions, read watermarks, optimistic send state.
|
|
507
|
+
*
|
|
508
|
+
* ```tsx
|
|
509
|
+
* const { messages, hasMore, store } = useMessages("room_123");
|
|
510
|
+
* // ...
|
|
511
|
+
* <button onClick={() => store.loadMore()} disabled={!hasMore}>more</button>
|
|
512
|
+
* {messages.map((m) => <Bubble key={m.id} message={m} />)}
|
|
513
|
+
* <input onKeyDown={(e) => e.key === "Enter" && store.send(draft)} />
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
declare function useMessages(roomId: string, opts?: UseMessagesOptions): UseMessagesResult;
|
|
517
|
+
|
|
518
|
+
interface UseMemberListResult extends MemberListSnapshot {
|
|
519
|
+
/** The backing store — `load`, `isOperator`. */
|
|
520
|
+
store: MemberListStore;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Live member list of a room — roles, presence dots, mute state. Reloads
|
|
524
|
+
* on membership events; presence transitions update in place.
|
|
525
|
+
*
|
|
526
|
+
* Combine with `useMessages` for read receipts:
|
|
527
|
+
* `activeMemberIds.length - 1 - store.readCount(messageId, activeMemberIds)`
|
|
528
|
+
* gives a KakaoTalk-style "unread N" per message.
|
|
529
|
+
*
|
|
530
|
+
* ```tsx
|
|
531
|
+
* const { members, operators } = useMemberList("room_123");
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
declare function useMemberList(roomId: string): UseMemberListResult;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The `Room` facade for `roomId`, from the nearest `NoveraChatProvider`'s
|
|
538
|
+
* client. Use it for direct SDK calls the hooks don't cover (members,
|
|
539
|
+
* announcements, invites, moderation, …).
|
|
540
|
+
*
|
|
541
|
+
* Deliberately NOT memoized: `chat.room()` is a get-or-create map lookup,
|
|
542
|
+
* and re-resolving each render guarantees the facade is never an orphan
|
|
543
|
+
* after the provider reconnects.
|
|
544
|
+
*
|
|
545
|
+
* ```tsx
|
|
546
|
+
* const room = useRoom("room_123");
|
|
547
|
+
* const members = await room.listMembers();
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
declare function useRoom(roomId: string): Room;
|
|
551
|
+
|
|
552
|
+
interface UseRoomFilesOptions {
|
|
553
|
+
/** Page size for both the initial load and `loadMore`. Default 50. */
|
|
554
|
+
pageSize?: number;
|
|
555
|
+
}
|
|
556
|
+
interface UseRoomFilesResult extends RoomFilesSnapshot {
|
|
557
|
+
/** The backing store — `refresh`, `loadMore`. */
|
|
558
|
+
store: RoomFilesStore;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Paginated media/file grid of a room (파일함·앨범). Newest first; call
|
|
562
|
+
* `store.loadMore()` from the grid's tail while `hasMore`, and
|
|
563
|
+
* `store.refresh()` after an upload to surface it at the head. Render
|
|
564
|
+
* thumbnails with `chat.fileThumbnailUrl(item.file.id, token)`.
|
|
565
|
+
*
|
|
566
|
+
* ```tsx
|
|
567
|
+
* const { media, hasMore, store } = useRoomFiles("room_123");
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
declare function useRoomFiles(roomId: string, opts?: UseRoomFilesOptions): UseRoomFilesResult;
|
|
571
|
+
|
|
572
|
+
interface UseRoomListResult extends RoomListSnapshot {
|
|
573
|
+
/** The backing store — `load`, `markRead`, `hideRoom`, `unhideRoom`. */
|
|
574
|
+
store: RoomListStore;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Live chat-room list (채팅 탭): previews, unread badges and
|
|
578
|
+
* most-recent-first ordering, kept fresh from WS streams.
|
|
579
|
+
*
|
|
580
|
+
* `opts.previewBuilder` / `opts.isRoomVisible` are read once (first
|
|
581
|
+
* render). Call `store.markRead(roomId)` when returning from a room screen
|
|
582
|
+
* to clear its badge.
|
|
583
|
+
*
|
|
584
|
+
* ```tsx
|
|
585
|
+
* const { rooms, totalUnread, store } = useRoomList();
|
|
586
|
+
* // ...
|
|
587
|
+
* {rooms.map((r) => (
|
|
588
|
+
* <li key={r.roomId} onClick={() => openRoom(r.roomId)}>
|
|
589
|
+
* {r.name} — {r.lastMessagePreview}
|
|
590
|
+
* {r.unreadCount > 0 && <b>{r.unreadCount}</b>}
|
|
591
|
+
* </li>
|
|
592
|
+
* ))}
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
declare function useRoomList(opts?: RoomListStoreOptions): UseRoomListResult;
|
|
596
|
+
|
|
597
|
+
interface UseRoomSettingsResult extends RoomSettingsSnapshot {
|
|
598
|
+
/** The backing store — `setMuted`, `setPushTrigger`, invite CRUD,
|
|
599
|
+
* join-request inbox, `leave` / `clearHistory` / `deleteMyMessages`,
|
|
600
|
+
* and operator moderation (`freeze`, `setPublic`, `muteMember`, …). */
|
|
601
|
+
store: RoomSettingsStore;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* State + actions behind a room-settings screen (대화방 설정). Lists are
|
|
605
|
+
* lazy — call `store.loadInvites()` / `store.loadJoinRequests()` when the
|
|
606
|
+
* relevant section opens (both are operator-only server-side).
|
|
607
|
+
*
|
|
608
|
+
* ```tsx
|
|
609
|
+
* const { invites, joinRequests, store } = useRoomSettings("room_123");
|
|
610
|
+
* // ...
|
|
611
|
+
* <Switch onChange={(on) => store.setMuted(on)} />
|
|
612
|
+
* <button onClick={() => store.leave({ deleteMessages: "mine" })}>나가기</button>
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
declare function useRoomSettings(roomId: string): UseRoomSettingsResult;
|
|
616
|
+
|
|
617
|
+
interface UseTypingOptions {
|
|
618
|
+
/** How long (ms) a user stays "typing" without a fresh event before
|
|
619
|
+
* auto-removal. Default 5000. */
|
|
620
|
+
timeoutMs?: number;
|
|
621
|
+
}
|
|
622
|
+
interface UseTypingResult {
|
|
623
|
+
/** User ids currently typing. */
|
|
624
|
+
typingUserIds: readonly string[];
|
|
625
|
+
isAnyoneTyping: boolean;
|
|
626
|
+
/** Broadcast the local user's typing state to the room (throttle it
|
|
627
|
+
* yourself — one `true` per keystroke burst, `false` on blur/send). */
|
|
628
|
+
setTyping: (isTyping: boolean) => void;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Who is currently typing in a room, for a "… is typing" indicator.
|
|
632
|
+
*
|
|
633
|
+
* The server sends a `typing: true` event when someone starts, but a
|
|
634
|
+
* "stopped" event isn't guaranteed to arrive. To avoid a stuck indicator,
|
|
635
|
+
* each user is auto-removed after `timeoutMs` unless a fresh typing event
|
|
636
|
+
* resets their timer.
|
|
637
|
+
*
|
|
638
|
+
* ```tsx
|
|
639
|
+
* const { isAnyoneTyping, setTyping } = useTyping("room_123");
|
|
640
|
+
* // ...
|
|
641
|
+
* <span>{isAnyoneTyping ? "typing…" : ""}</span>
|
|
642
|
+
* <input onChange={() => setTyping(true)} onBlur={() => setTyping(false)} />
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
declare function useTyping(roomId: string, opts?: UseTypingOptions): UseTypingResult;
|
|
646
|
+
|
|
647
|
+
interface UseUnreadOptions {
|
|
648
|
+
/** Poll `chat.unreadSummary()` every N ms. Off by default (pull model —
|
|
649
|
+
* call `refresh()` on focus / after reading a room). The endpoint is a
|
|
650
|
+
* single cheap REST call, so a few seconds is a fine badge cadence. */
|
|
651
|
+
refreshIntervalMs?: number;
|
|
652
|
+
}
|
|
653
|
+
interface UseUnreadResult {
|
|
654
|
+
/** Total unread across all rooms. 0 until the first fetch resolves. */
|
|
655
|
+
total: number;
|
|
656
|
+
/** The full last-fetched summary (per-room breakdown in `rooms`),
|
|
657
|
+
* or null before the first fetch. */
|
|
658
|
+
summary: UnreadSummary | null;
|
|
659
|
+
/** Fetch the latest unread summary now. */
|
|
660
|
+
refresh: () => Promise<void>;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The account-wide unread summary, for a badge. Fetches once on mount;
|
|
664
|
+
* refresh manually via `refresh()` or periodically via
|
|
665
|
+
* `refreshIntervalMs`.
|
|
666
|
+
*
|
|
667
|
+
* ```tsx
|
|
668
|
+
* const { total, refresh } = useUnread({ refreshIntervalMs: 15_000 });
|
|
669
|
+
* // ...
|
|
670
|
+
* <Badge count={total} />
|
|
671
|
+
* ```
|
|
672
|
+
*/
|
|
673
|
+
declare function useUnread(opts?: UseUnreadOptions): UseUnreadResult;
|
|
674
|
+
|
|
675
|
+
export { type ChatMessage, type ChatMessageStatus, type MemberListSnapshot, MemberListStore, NoveraChatProvider, type NoveraChatProviderProps, type RoomFileItem, type RoomFilesSnapshot, RoomFilesStore, type RoomListItem, type RoomListSnapshot, RoomListStore, type RoomListStoreOptions, type RoomSettingsSnapshot, RoomSettingsStore, type RoomSnapshot, RoomStore, type UseMemberListResult, type UseMessagesOptions, type UseMessagesResult, type UseRoomFilesOptions, type UseRoomFilesResult, type UseRoomListResult, type UseRoomSettingsResult, type UseTypingOptions, type UseTypingResult, type UseUnreadOptions, type UseUnreadResult, chatMessageFromHistory, chatMessageFromLive, useMemberList, useMessages, useNoveraChat, useRoom, useRoomFiles, useRoomList, useRoomSettings, useTyping, useUnread };
|