mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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.
105 lines
3.0 KiB
TypeScript
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,
|
|
}
|
|
}
|