Files
hapi/web/src/hooks/mutations/useSendMessage.ts
T
weishu 0f29ee182c refactor: replace React Query message cache with windowed message store
Introduces a new message-window-store module to manage message state with
automatic windowing of visible and pending messages. This replaces manual
React Query cache operations with a centralized, observable state system.
close #39

Key changes:
- New MessageWindowState tracks visible/pending messages with size limits
- Automatic trimming of message windows (400 visible, 200 pending messages)
- Pending message buffering when user scrolls away from bottom
- Centralized status updates for optimistic messages
- Thread component simplified with forwarded scroll and pending callbacks
- Removes message count tracking from components

This improves performance for chats with many messages and provides a
cleaner separation of concerns between UI and state management.
2026-01-07 16:59:12 +08:00

105 lines
3.0 KiB
TypeScript

import { useMutation } from '@tanstack/react-query'
import type { ApiClient } from '@/api/client'
import type { DecryptedMessage } from '@/types/api'
import { makeClientSideId } from '@/lib/messages'
import {
appendOptimisticMessage,
getMessageWindowState,
updateMessageStatus,
} from '@/lib/message-window-store'
import { usePlatform } from '@/hooks/usePlatform'
type SendMessageInput = {
sessionId: string
text: string
localId: string
createdAt: number
}
function findMessageByLocalId(
sessionId: string,
localId: string,
): DecryptedMessage | null {
const state = getMessageWindowState(sessionId)
for (const message of state.messages) {
if (message.localId === localId) return message
}
for (const message of state.pending) {
if (message.localId === localId) return message
}
return null
}
export function useSendMessage(api: ApiClient | null, sessionId: string | null): {
sendMessage: (text: string) => void
retryMessage: (localId: string) => void
isSending: boolean
} {
const { haptic } = usePlatform()
const mutation = useMutation({
mutationFn: async (input: SendMessageInput) => {
if (!api) {
throw new Error('API unavailable')
}
await api.sendMessage(input.sessionId, input.text, input.localId)
},
onMutate: async (input) => {
const optimisticMessage: DecryptedMessage = {
id: input.localId,
seq: null,
localId: input.localId,
content: { role: 'user', content: input.text },
createdAt: input.createdAt,
status: 'sending',
originalText: input.text,
}
appendOptimisticMessage(input.sessionId, optimisticMessage)
},
onSuccess: (_, input) => {
updateMessageStatus(input.sessionId, input.localId, 'sent')
haptic.notification('success')
},
onError: (_, input) => {
updateMessageStatus(input.sessionId, input.localId, 'failed')
haptic.notification('error')
},
})
const sendMessage = (text: string) => {
if (!api || !sessionId) return
if (mutation.isPending) return
const localId = makeClientSideId('local')
mutation.mutate({
sessionId,
text,
localId,
createdAt: Date.now(),
})
}
const retryMessage = (localId: string) => {
if (!api || !sessionId) return
if (mutation.isPending) return
const message = findMessageByLocalId(sessionId, localId)
if (!message?.originalText) return
updateMessageStatus(sessionId, localId, 'sending')
mutation.mutate({
sessionId,
text: message.originalText,
localId,
createdAt: message.createdAt,
})
}
return {
sendMessage,
retryMessage,
isSending: mutation.isPending,
}
}