@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
import { useNoveraChat } from "./context.js";
|
|
4
|
+
import {
|
|
5
|
+
RoomSettingsStore,
|
|
6
|
+
type RoomSettingsSnapshot,
|
|
7
|
+
} from "./room-settings-store.js";
|
|
8
|
+
|
|
9
|
+
export interface UseRoomSettingsResult extends RoomSettingsSnapshot {
|
|
10
|
+
/** The backing store — `setMuted`, `setPushTrigger`, invite CRUD,
|
|
11
|
+
* join-request inbox, `leave` / `clearHistory` / `deleteMyMessages`,
|
|
12
|
+
* and operator moderation (`freeze`, `setPublic`, `muteMember`, …). */
|
|
13
|
+
store: RoomSettingsStore;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* State + actions behind a room-settings screen (대화방 설정). Lists are
|
|
18
|
+
* lazy — call `store.loadInvites()` / `store.loadJoinRequests()` when the
|
|
19
|
+
* relevant section opens (both are operator-only server-side).
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const { invites, joinRequests, store } = useRoomSettings("room_123");
|
|
23
|
+
* // ...
|
|
24
|
+
* <Switch onChange={(on) => store.setMuted(on)} />
|
|
25
|
+
* <button onClick={() => store.leave({ deleteMessages: "mine" })}>나가기</button>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function useRoomSettings(roomId: string): UseRoomSettingsResult {
|
|
29
|
+
const chat = useNoveraChat();
|
|
30
|
+
const store = useMemo(
|
|
31
|
+
() => new RoomSettingsStore(chat, roomId),
|
|
32
|
+
[chat, roomId],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
store.attach();
|
|
37
|
+
return () => store.dispose();
|
|
38
|
+
}, [store]);
|
|
39
|
+
|
|
40
|
+
const snapshot = useSyncExternalStore(
|
|
41
|
+
store.subscribe,
|
|
42
|
+
store.getSnapshot,
|
|
43
|
+
store.getSnapshot,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { ...snapshot, store };
|
|
47
|
+
}
|
package/src/use-room.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Room } from "@noverachat/sdk-web";
|
|
2
|
+
|
|
3
|
+
import { useNoveraChat } from "./context.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `Room` facade for `roomId`, from the nearest `NoveraChatProvider`'s
|
|
7
|
+
* client. Use it for direct SDK calls the hooks don't cover (members,
|
|
8
|
+
* announcements, invites, moderation, …).
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NOT memoized: `chat.room()` is a get-or-create map lookup,
|
|
11
|
+
* and re-resolving each render guarantees the facade is never an orphan
|
|
12
|
+
* after the provider reconnects.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const room = useRoom("room_123");
|
|
16
|
+
* const members = await room.listMembers();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function useRoom(roomId: string): Room {
|
|
20
|
+
return useNoveraChat().room(roomId);
|
|
21
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useNoveraChat } from "./context.js";
|
|
4
|
+
|
|
5
|
+
export interface UseTypingOptions {
|
|
6
|
+
/** How long (ms) a user stays "typing" without a fresh event before
|
|
7
|
+
* auto-removal. Default 5000. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseTypingResult {
|
|
12
|
+
/** User ids currently typing. */
|
|
13
|
+
typingUserIds: readonly string[];
|
|
14
|
+
isAnyoneTyping: boolean;
|
|
15
|
+
/** Broadcast the local user's typing state to the room (throttle it
|
|
16
|
+
* yourself — one `true` per keystroke burst, `false` on blur/send). */
|
|
17
|
+
setTyping: (isTyping: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Who is currently typing in a room, for a "… is typing" indicator.
|
|
22
|
+
*
|
|
23
|
+
* The server sends a `typing: true` event when someone starts, but a
|
|
24
|
+
* "stopped" event isn't guaranteed to arrive. To avoid a stuck indicator,
|
|
25
|
+
* each user is auto-removed after `timeoutMs` unless a fresh typing event
|
|
26
|
+
* resets their timer.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const { isAnyoneTyping, setTyping } = useTyping("room_123");
|
|
30
|
+
* // ...
|
|
31
|
+
* <span>{isAnyoneTyping ? "typing…" : ""}</span>
|
|
32
|
+
* <input onChange={() => setTyping(true)} onBlur={() => setTyping(false)} />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function useTyping(
|
|
36
|
+
roomId: string,
|
|
37
|
+
opts?: UseTypingOptions,
|
|
38
|
+
): UseTypingResult {
|
|
39
|
+
const chat = useNoveraChat();
|
|
40
|
+
const timeoutMs = opts?.timeoutMs ?? 5000;
|
|
41
|
+
const [typingUserIds, setTypingUserIds] = useState<readonly string[]>([]);
|
|
42
|
+
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const room = chat.room(roomId);
|
|
46
|
+
const timerMap = timers.current;
|
|
47
|
+
|
|
48
|
+
const stop = (userId: string): void => {
|
|
49
|
+
const t = timerMap.get(userId);
|
|
50
|
+
if (t) clearTimeout(t);
|
|
51
|
+
timerMap.delete(userId);
|
|
52
|
+
setTypingUserIds((cur) =>
|
|
53
|
+
cur.includes(userId) ? cur.filter((u) => u !== userId) : cur,
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const start = (userId: string): void => {
|
|
58
|
+
// (Re)start the auto-remove timer on every typing event.
|
|
59
|
+
const t = timerMap.get(userId);
|
|
60
|
+
if (t) clearTimeout(t);
|
|
61
|
+
timerMap.set(userId, setTimeout(() => stop(userId), timeoutMs));
|
|
62
|
+
setTypingUserIds((cur) =>
|
|
63
|
+
cur.includes(userId) ? cur : [...cur, userId],
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const unsub = room.on("typing", (f) => {
|
|
68
|
+
if (f.is_typing) start(f.user_id);
|
|
69
|
+
else stop(f.user_id);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
unsub();
|
|
74
|
+
for (const t of timerMap.values()) clearTimeout(t);
|
|
75
|
+
timerMap.clear();
|
|
76
|
+
setTypingUserIds([]);
|
|
77
|
+
};
|
|
78
|
+
}, [chat, roomId, timeoutMs]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
typingUserIds,
|
|
82
|
+
isAnyoneTyping: typingUserIds.length > 0,
|
|
83
|
+
setTyping: (isTyping: boolean) => chat.room(roomId).setTyping(isTyping),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { UnreadSummary } from "@noverachat/sdk-web";
|
|
3
|
+
|
|
4
|
+
import { useNoveraChat } from "./context.js";
|
|
5
|
+
|
|
6
|
+
export interface UseUnreadOptions {
|
|
7
|
+
/** Poll `chat.unreadSummary()` every N ms. Off by default (pull model —
|
|
8
|
+
* call `refresh()` on focus / after reading a room). The endpoint is a
|
|
9
|
+
* single cheap REST call, so a few seconds is a fine badge cadence. */
|
|
10
|
+
refreshIntervalMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseUnreadResult {
|
|
14
|
+
/** Total unread across all rooms. 0 until the first fetch resolves. */
|
|
15
|
+
total: number;
|
|
16
|
+
/** The full last-fetched summary (per-room breakdown in `rooms`),
|
|
17
|
+
* or null before the first fetch. */
|
|
18
|
+
summary: UnreadSummary | null;
|
|
19
|
+
/** Fetch the latest unread summary now. */
|
|
20
|
+
refresh: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The account-wide unread summary, for a badge. Fetches once on mount;
|
|
25
|
+
* refresh manually via `refresh()` or periodically via
|
|
26
|
+
* `refreshIntervalMs`.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const { total, refresh } = useUnread({ refreshIntervalMs: 15_000 });
|
|
30
|
+
* // ...
|
|
31
|
+
* <Badge count={total} />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useUnread(opts?: UseUnreadOptions): UseUnreadResult {
|
|
35
|
+
const chat = useNoveraChat();
|
|
36
|
+
const refreshIntervalMs = opts?.refreshIntervalMs;
|
|
37
|
+
const [summary, setSummary] = useState<UnreadSummary | null>(null);
|
|
38
|
+
|
|
39
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
40
|
+
setSummary(await chat.unreadSummary());
|
|
41
|
+
}, [chat]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let alive = true;
|
|
45
|
+
const tick = async (): Promise<void> => {
|
|
46
|
+
try {
|
|
47
|
+
const s = await chat.unreadSummary();
|
|
48
|
+
if (alive) setSummary(s);
|
|
49
|
+
} catch {
|
|
50
|
+
// Badge data — swallow transient fetch errors, next tick retries.
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
void tick();
|
|
54
|
+
const timer =
|
|
55
|
+
refreshIntervalMs != null && refreshIntervalMs > 0
|
|
56
|
+
? setInterval(() => void tick(), refreshIntervalMs)
|
|
57
|
+
: null;
|
|
58
|
+
return () => {
|
|
59
|
+
alive = false;
|
|
60
|
+
if (timer) clearInterval(timer);
|
|
61
|
+
};
|
|
62
|
+
}, [chat, refreshIntervalMs]);
|
|
63
|
+
|
|
64
|
+
return { total: summary?.total ?? 0, summary, refresh };
|
|
65
|
+
}
|