From 763f06ab8c18f36354ba84baf2cc710c4963a98d Mon Sep 17 00:00:00 2001 From: root Date: Tue, 12 May 2026 16:04:25 +0000 Subject: [PATCH] feat(frontend): add typing presence and reconnect repull realtime behavior --- src/pages/ChatPage.tsx | 51 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 1ac6de8..afd8bc6 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -24,6 +24,8 @@ export default function ChatPage() { const [messages, setMessages] = useState([]) const [pageInfo, setPageInfo] = useState(null) const [socketState, setSocketState] = useState<'offline' | 'online'>('offline') + const [onlineCount, setOnlineCount] = useState(0) + const [typingUsers, setTypingUsers] = useState([]) const [seqFrom, setSeqFrom] = useState('1') const [seqTo, setSeqTo] = useState('999999') const [limit, setLimit] = useState('50') @@ -33,8 +35,38 @@ export default function ChatPage() { const socket = useMemo(() => getSocketClient('frontend-user'), []) useEffect(() => { - socket.on('connect', () => setSocketState('online')) + socket.on('connect', () => { + setSocketState('online') + if (!channelId) return + socket.emit('join_channel', { channelId }) + void getApiClient() + .get(`/channels/${channelId}/messages`, { + params: { + seq_from: Number(seqFrom || '1'), + seq_to: Number(seqTo || '999999'), + limit: Number(limit || '50'), + }, + }) + .then((res) => { + setMessages(res.data.items ?? []) + setPageInfo(res.data.page ?? null) + }) + }) socket.on('disconnect', () => setSocketState('offline')) + socket.on('presence.online', (e: { onlineCount?: number }) => { + setOnlineCount(e.onlineCount ?? 0) + }) + socket.on('presence.offline', (e: { onlineCount?: number }) => { + setOnlineCount(e.onlineCount ?? 0) + }) + socket.on('typing.start', (e: { channelId?: string; userId?: string }) => { + if (!e.channelId || e.channelId !== channelId || !e.userId || e.userId === 'frontend-user') return + setTypingUsers((prev) => (prev.includes(e.userId as string) ? prev : [...prev, e.userId as string])) + }) + socket.on('typing.stop', (e: { channelId?: string; userId?: string }) => { + if (!e.channelId || e.channelId !== channelId || !e.userId) return + setTypingUsers((prev) => prev.filter((x) => x !== e.userId)) + }) socket.on('message.created', (m: MessageItem) => setMessages((prev) => [...prev, m])) socket.on('message.updated', (m: MessageItem) => setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))), @@ -50,7 +82,7 @@ export default function ChatPage() { socket.removeAllListeners() disconnectSocket() } - }, [socket]) + }, [socket, channelId, seqFrom, seqTo, limit]) async function pullMessages() { if (!channelId) return @@ -71,6 +103,7 @@ export default function ChatPage() { content, authorUserId: 'frontend-user', }) + socket.emit('typing.stop', { channelId }) setContent('') } @@ -96,6 +129,16 @@ export default function ChatPage() { if (value) next.set('channelId', value) else next.delete('channelId') setSearchParams(next) + setTypingUsers([]) + } + + function onTypingChange(value: string) { + const wasEmpty = !content.trim() + const nowEmpty = !value.trim() + setContent(value) + if (!channelId) return + if (wasEmpty && !nowEmpty) socket.emit('typing.start', { channelId }) + if (!wasEmpty && nowEmpty) socket.emit('typing.stop', { channelId }) } useEffect(() => { @@ -111,6 +154,8 @@ export default function ChatPage() {

聊天

Guild: {guildId || '-'}

Socket: {socketState}

+

在线人数: {onlineCount}

+ {typingUsers.length ?

正在输入: {typingUsers.join(', ')}

: null}
onChangeChannel(e.target.value)} placeholder="Channel ID" /> setSeqFrom(e.target.value)} placeholder="seq_from" /> @@ -119,7 +164,7 @@ export default function ChatPage() {
- setContent(e.target.value)} placeholder="输入消息" /> + onTypingChange(e.target.value)} placeholder="输入消息" />