feat(frontend): add typing presence and reconnect repull realtime behavior
This commit is contained in:
@@ -24,6 +24,8 @@ export default function ChatPage() {
|
|||||||
const [messages, setMessages] = useState<MessageItem[]>([])
|
const [messages, setMessages] = useState<MessageItem[]>([])
|
||||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null)
|
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null)
|
||||||
const [socketState, setSocketState] = useState<'offline' | 'online'>('offline')
|
const [socketState, setSocketState] = useState<'offline' | 'online'>('offline')
|
||||||
|
const [onlineCount, setOnlineCount] = useState(0)
|
||||||
|
const [typingUsers, setTypingUsers] = useState<string[]>([])
|
||||||
const [seqFrom, setSeqFrom] = useState('1')
|
const [seqFrom, setSeqFrom] = useState('1')
|
||||||
const [seqTo, setSeqTo] = useState('999999')
|
const [seqTo, setSeqTo] = useState('999999')
|
||||||
const [limit, setLimit] = useState('50')
|
const [limit, setLimit] = useState('50')
|
||||||
@@ -33,8 +35,38 @@ export default function ChatPage() {
|
|||||||
const socket = useMemo(() => getSocketClient('frontend-user'), [])
|
const socket = useMemo(() => getSocketClient('frontend-user'), [])
|
||||||
|
|
||||||
useEffect(() => {
|
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('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.created', (m: MessageItem) => setMessages((prev) => [...prev, m]))
|
||||||
socket.on('message.updated', (m: MessageItem) =>
|
socket.on('message.updated', (m: MessageItem) =>
|
||||||
setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))),
|
setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))),
|
||||||
@@ -50,7 +82,7 @@ export default function ChatPage() {
|
|||||||
socket.removeAllListeners()
|
socket.removeAllListeners()
|
||||||
disconnectSocket()
|
disconnectSocket()
|
||||||
}
|
}
|
||||||
}, [socket])
|
}, [socket, channelId, seqFrom, seqTo, limit])
|
||||||
|
|
||||||
async function pullMessages() {
|
async function pullMessages() {
|
||||||
if (!channelId) return
|
if (!channelId) return
|
||||||
@@ -71,6 +103,7 @@ export default function ChatPage() {
|
|||||||
content,
|
content,
|
||||||
authorUserId: 'frontend-user',
|
authorUserId: 'frontend-user',
|
||||||
})
|
})
|
||||||
|
socket.emit('typing.stop', { channelId })
|
||||||
setContent('')
|
setContent('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +129,16 @@ export default function ChatPage() {
|
|||||||
if (value) next.set('channelId', value)
|
if (value) next.set('channelId', value)
|
||||||
else next.delete('channelId')
|
else next.delete('channelId')
|
||||||
setSearchParams(next)
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -111,6 +154,8 @@ export default function ChatPage() {
|
|||||||
<h2>聊天</h2>
|
<h2>聊天</h2>
|
||||||
<p>Guild: {guildId || '-'}</p>
|
<p>Guild: {guildId || '-'}</p>
|
||||||
<p>Socket: {socketState}</p>
|
<p>Socket: {socketState}</p>
|
||||||
|
<p>在线人数: {onlineCount}</p>
|
||||||
|
{typingUsers.length ? <p>正在输入: {typingUsers.join(', ')}</p> : null}
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||||
<input value={channelId} onChange={(e) => onChangeChannel(e.target.value)} placeholder="Channel ID" />
|
<input value={channelId} onChange={(e) => onChangeChannel(e.target.value)} placeholder="Channel ID" />
|
||||||
<input value={seqFrom} onChange={(e) => setSeqFrom(e.target.value)} placeholder="seq_from" />
|
<input value={seqFrom} onChange={(e) => setSeqFrom(e.target.value)} placeholder="seq_from" />
|
||||||
@@ -119,7 +164,7 @@ export default function ChatPage() {
|
|||||||
<button onClick={pullMessages}>拉取</button>
|
<button onClick={pullMessages}>拉取</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||||
<input value={content} onChange={(e) => setContent(e.target.value)} placeholder="输入消息" />
|
<input value={content} onChange={(e) => onTypingChange(e.target.value)} placeholder="输入消息" />
|
||||||
<button onClick={sendMessage}>发送</button>
|
<button onClick={sendMessage}>发送</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user