|
|
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
import { Message } from '../types'
|
|
|
import { api } from '../services/api'
|
|
|
import { logger } from '../utils/logger'
|
|
|
+import { isNotificationLikeMessage, isMessageFromSelf } from '../utils/messageTypes'
|
|
|
|
|
|
export function useMessages(
|
|
|
token: string,
|
|
|
@@ -18,77 +19,32 @@ export function useMessages(
|
|
|
const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({})
|
|
|
const isLoadingMoreRef = useRef(false)
|
|
|
|
|
|
- // 获取指定联系人的消息列表
|
|
|
- const fetchMessages = useCallback(async (contactId: number) => {
|
|
|
- if (!token || loadedContacts.has(contactId)) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- setIsLoadingMessages(true)
|
|
|
- try {
|
|
|
- const messagesData = await api.getMessages(token, contactId, {
|
|
|
- skip: 0,
|
|
|
- limit: 50
|
|
|
- })
|
|
|
-
|
|
|
- logger.info('App: Fetched messages', { contactId, count: messagesData.length })
|
|
|
-
|
|
|
- const formattedMessages: Message[] = messagesData.map(msg => {
|
|
|
- const isNotification = msg.type === 'NOTIFICATION'
|
|
|
- const isSelf = !isNotification && currentUserId !== null && msg.sender_id === currentUserId
|
|
|
-
|
|
|
- let messageType: 'text' | 'image' | 'file' | 'video' | 'notification' = 'text'
|
|
|
- if (isNotification) {
|
|
|
- messageType = 'notification'
|
|
|
- } else if (msg.content_type === 'IMAGE') {
|
|
|
- messageType = 'image'
|
|
|
- } else if (msg.content_type === 'VIDEO') {
|
|
|
- messageType = 'video'
|
|
|
- } else if (msg.content_type === 'FILE') {
|
|
|
- messageType = 'file'
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- id: msg.id,
|
|
|
- content: msg.content || msg.title || '',
|
|
|
- isSelf: isSelf,
|
|
|
- type: messageType,
|
|
|
- timestamp: new Date(msg.created_at).getTime(),
|
|
|
- title: msg.title,
|
|
|
- actionUrl: msg.action_url,
|
|
|
- actionText: msg.action_text,
|
|
|
- messageType: msg.type,
|
|
|
- content_type: msg.content_type,
|
|
|
- size: (msg as any).size
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- formattedMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
|
-
|
|
|
- setMessages(prev => ({
|
|
|
- ...prev,
|
|
|
- [contactId]: formattedMessages
|
|
|
- }))
|
|
|
-
|
|
|
- setHasMoreMessages(prev => ({
|
|
|
- ...prev,
|
|
|
- [contactId]: messagesData.length >= 50
|
|
|
- }))
|
|
|
-
|
|
|
- setLoadedContacts(prev => new Set(prev).add(contactId))
|
|
|
- } catch (error: any) {
|
|
|
- logger.error('App: Failed to fetch messages', { contactId, error: error.message })
|
|
|
- } finally {
|
|
|
+ // 退出登录或切换账号时清空内存中的会话消息,避免同一 contactId 跨账号串数据
|
|
|
+ useEffect(() => {
|
|
|
+ if (!token) {
|
|
|
+ setMessages({})
|
|
|
+ setLoadedContacts(new Set())
|
|
|
+ setHasMoreMessages({})
|
|
|
+ setUploadProgress({})
|
|
|
+ isLoadingMoreRef.current = false
|
|
|
setIsLoadingMessages(false)
|
|
|
+ setIsLoadingMore(false)
|
|
|
+ return
|
|
|
}
|
|
|
- }, [token, currentUserId, loadedContacts])
|
|
|
+ setMessages({})
|
|
|
+ setLoadedContacts(new Set())
|
|
|
+ setHasMoreMessages({})
|
|
|
+ setUploadProgress({})
|
|
|
+ isLoadingMoreRef.current = false
|
|
|
+ setIsLoadingMore(false)
|
|
|
+ }, [token, currentUserId])
|
|
|
|
|
|
const formatMessageData = useCallback((msg: any): Message => {
|
|
|
- const isNotification = msg.type === 'NOTIFICATION'
|
|
|
- const isSelf = !isNotification && currentUserId !== null && msg.sender_id === currentUserId
|
|
|
+ const notificationLike = isNotificationLikeMessage(msg.type, msg.content_type)
|
|
|
+ const isSelf = isMessageFromSelf(msg, currentUserId)
|
|
|
|
|
|
let messageType: 'text' | 'image' | 'file' | 'video' | 'notification' = 'text'
|
|
|
- if (isNotification) {
|
|
|
+ if (notificationLike) {
|
|
|
messageType = 'notification'
|
|
|
} else if (msg.content_type === 'IMAGE') {
|
|
|
messageType = 'image'
|
|
|
@@ -113,6 +69,54 @@ export function useMessages(
|
|
|
}
|
|
|
}, [currentUserId])
|
|
|
|
|
|
+ // 拉取该会话最新一页;切换会话时也会调用,并与本地已加载的更早历史、未入库的本地消息合并
|
|
|
+ const refreshMessages = useCallback(async (contactId: number) => {
|
|
|
+ if (!token) return
|
|
|
+
|
|
|
+ setIsLoadingMessages(true)
|
|
|
+ try {
|
|
|
+ const messagesData = await api.getMessages(token, contactId, {
|
|
|
+ skip: 0,
|
|
|
+ limit: 50
|
|
|
+ })
|
|
|
+
|
|
|
+ logger.info('App: Refreshed messages', { contactId, count: messagesData.length })
|
|
|
+
|
|
|
+ const formattedMessages: Message[] = messagesData.map(formatMessageData)
|
|
|
+ formattedMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
|
+
|
|
|
+ setMessages(prev => {
|
|
|
+ const existing = prev[contactId] || []
|
|
|
+ if (formattedMessages.length === 0) {
|
|
|
+ return { ...prev, [contactId]: existing }
|
|
|
+ }
|
|
|
+ const serverIds = new Set(formattedMessages.map(m => m.id))
|
|
|
+ const oldestFetched = formattedMessages[0].timestamp
|
|
|
+ const preservedOlder = existing.filter(
|
|
|
+ m => !serverIds.has(m.id) && m.timestamp < oldestFetched
|
|
|
+ )
|
|
|
+ const pendingLocal = existing.filter(
|
|
|
+ m => !serverIds.has(m.id) && m.timestamp >= oldestFetched
|
|
|
+ )
|
|
|
+ const merged = [...preservedOlder, ...formattedMessages, ...pendingLocal].sort(
|
|
|
+ (a, b) => a.timestamp - b.timestamp
|
|
|
+ )
|
|
|
+ return { ...prev, [contactId]: merged }
|
|
|
+ })
|
|
|
+
|
|
|
+ setHasMoreMessages(prev => ({
|
|
|
+ ...prev,
|
|
|
+ [contactId]: messagesData.length >= 50
|
|
|
+ }))
|
|
|
+
|
|
|
+ setLoadedContacts(prev => new Set(prev).add(contactId))
|
|
|
+ } catch (error: any) {
|
|
|
+ logger.error('App: Failed to refresh messages', { contactId, error: error.message })
|
|
|
+ } finally {
|
|
|
+ setIsLoadingMessages(false)
|
|
|
+ }
|
|
|
+ }, [token, formatMessageData])
|
|
|
+
|
|
|
const fetchMoreMessages = useCallback(async (contactId: number) => {
|
|
|
if (!token || isLoadingMoreRef.current || !hasMoreMessages[contactId]) return
|
|
|
|
|
|
@@ -183,7 +187,9 @@ export function useMessages(
|
|
|
logger.info('App: Message sent successfully', { content, receiverId: contactId, messageId: response.id })
|
|
|
|
|
|
let messageType: 'text' | 'image' | 'file' | 'video' | 'notification' = 'text'
|
|
|
- if (response.content_type === 'IMAGE') {
|
|
|
+ if (isNotificationLikeMessage(response.type, response.content_type)) {
|
|
|
+ messageType = 'notification'
|
|
|
+ } else if (response.content_type === 'IMAGE') {
|
|
|
messageType = 'image'
|
|
|
} else if (response.content_type === 'VIDEO') {
|
|
|
messageType = 'video'
|
|
|
@@ -199,7 +205,9 @@ export function useMessages(
|
|
|
timestamp: new Date(response.created_at).getTime(),
|
|
|
messageType: response.type,
|
|
|
content_type: response.content_type,
|
|
|
- title: response.title
|
|
|
+ title: response.title,
|
|
|
+ actionUrl: response.action_url,
|
|
|
+ actionText: response.action_text
|
|
|
}
|
|
|
|
|
|
setMessages(prev => ({
|
|
|
@@ -290,13 +298,12 @@ export function useMessages(
|
|
|
}
|
|
|
}, [activeContactId, setContacts, unreadCountsRef])
|
|
|
|
|
|
- // 切换联系人时获取消息
|
|
|
+ // 切换联系人时重新拉取该会话最新消息(含再次进入已打开过的会话)
|
|
|
useEffect(() => {
|
|
|
- if (activeContactId && token && !loadedContacts.has(activeContactId)) {
|
|
|
- fetchMessages(activeContactId)
|
|
|
+ if (activeContactId && token) {
|
|
|
+ void refreshMessages(activeContactId)
|
|
|
}
|
|
|
- // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- }, [activeContactId, token])
|
|
|
+ }, [activeContactId, token, refreshMessages])
|
|
|
|
|
|
return {
|
|
|
messages,
|
|
|
@@ -307,7 +314,7 @@ export function useMessages(
|
|
|
uploadProgress,
|
|
|
sendMessage,
|
|
|
sendFileMessage,
|
|
|
- fetchMessages,
|
|
|
+ fetchMessages: refreshMessages,
|
|
|
fetchMoreMessages,
|
|
|
loadedContacts
|
|
|
}
|