mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
Merge work into dev and resolve ChatPage.tsx by accepting work changes
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -42,6 +42,10 @@ release
|
||||
# OS
|
||||
Thumbs.db
|
||||
|
||||
# Electron dev cache
|
||||
.electron/
|
||||
.cache/
|
||||
|
||||
|
||||
|
||||
# 忽略 Visual Studio 临时文件夹
|
||||
@@ -50,4 +54,4 @@ Thumbs.db
|
||||
*.ipch
|
||||
*.aps
|
||||
|
||||
wcdb/
|
||||
wcdb/
|
||||
|
||||
@@ -5,25 +5,6 @@ import type { ChatSession, Message } from '../types/models'
|
||||
import { getEmojiPath } from 'wechat-emojis'
|
||||
import './ChatPage.scss'
|
||||
|
||||
const SESSION_MESSAGE_CACHE_LIMIT = 150
|
||||
const SESSION_MESSAGE_CACHE_MAX_ENTRIES = 200
|
||||
const sessionMessageCache = new Map<string, Message[]>()
|
||||
|
||||
const cacheSessionMessages = (sessionId: string, messages: Message[]) => {
|
||||
if (!sessionId) return
|
||||
const trimmed = messages.length > SESSION_MESSAGE_CACHE_LIMIT
|
||||
? messages.slice(-SESSION_MESSAGE_CACHE_LIMIT)
|
||||
: messages.slice()
|
||||
sessionMessageCache.set(sessionId, trimmed)
|
||||
if (sessionMessageCache.size > SESSION_MESSAGE_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = sessionMessageCache.keys().next().value
|
||||
if (oldestKey) {
|
||||
sessionMessageCache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface ChatPageProps {
|
||||
// 保留接口以备将来扩展
|
||||
}
|
||||
@@ -42,6 +23,66 @@ interface SessionDetail {
|
||||
messageTables: { dbName: string; tableName: string; count: number }[]
|
||||
}
|
||||
|
||||
// 全局头像加载队列管理器(限制并发,避免卡顿)
|
||||
class AvatarLoadQueue {
|
||||
private queue: Array<{ url: string; resolve: () => void; reject: () => void }> = []
|
||||
private loading = new Set<string>()
|
||||
private readonly maxConcurrent = 1 // 一次只加载1个头像,避免卡顿
|
||||
private readonly delayBetweenBatches = 100 // 批次间延迟100ms,给UI喘息时间
|
||||
|
||||
async enqueue(url: string): Promise<void> {
|
||||
// 如果已经在加载中,直接返回
|
||||
if (this.loading.has(url)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ url, resolve, reject })
|
||||
this.processQueue()
|
||||
})
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
// 如果已达到最大并发数,等待
|
||||
if (this.loading.size >= this.maxConcurrent) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果队列为空,返回
|
||||
if (this.queue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 取出一个任务
|
||||
const task = this.queue.shift()
|
||||
if (!task) return
|
||||
|
||||
this.loading.add(task.url)
|
||||
|
||||
// 加载图片
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
this.loading.delete(task.url)
|
||||
task.resolve()
|
||||
// 延迟一下再处理下一个,避免一次性加载太多
|
||||
setTimeout(() => this.processQueue(), this.delayBetweenBatches)
|
||||
}
|
||||
img.onerror = () => {
|
||||
this.loading.delete(task.url)
|
||||
task.reject()
|
||||
setTimeout(() => this.processQueue(), this.delayBetweenBatches)
|
||||
}
|
||||
img.src = task.url
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.queue = []
|
||||
this.loading.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const avatarLoadQueue = new AvatarLoadQueue()
|
||||
|
||||
// 头像组件 - 支持骨架屏加载和懒加载(优化:限制并发,使用 memo 避免不必要的重渲染)
|
||||
// 会话项组件(使用 memo 优化,避免不必要的重渲染)
|
||||
const SessionItem = React.memo(function SessionItem({
|
||||
@@ -56,7 +97,7 @@ const SessionItem = React.memo(function SessionItem({
|
||||
formatTime: (timestamp: number) => string
|
||||
}) {
|
||||
// 缓存格式化的时间
|
||||
const timeText = useMemo(() =>
|
||||
const timeText = useMemo(() =>
|
||||
formatTime(session.lastTimestamp || session.sortTimestamp),
|
||||
[formatTime, session.lastTimestamp, session.sortTimestamp]
|
||||
)
|
||||
@@ -101,6 +142,7 @@ const SessionAvatar = React.memo(function SessionAvatar({ session, size = 48 }:
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
const [isInQueue, setIsInQueue] = useState(false)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isGroup = session.username.includes('@chatroom')
|
||||
@@ -112,13 +154,59 @@ const SessionAvatar = React.memo(function SessionAvatar({ session, size = 48 }:
|
||||
return chars[0] || '?'
|
||||
}
|
||||
|
||||
// 使用 Intersection Observer 实现懒加载(优化性能)
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || shouldLoad || isInQueue) return
|
||||
if (!session.avatarUrl) {
|
||||
// 没有头像URL,不需要加载
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !isInQueue) {
|
||||
// 加入加载队列,而不是立即加载
|
||||
setIsInQueue(true)
|
||||
avatarLoadQueue.enqueue(session.avatarUrl!).then(() => {
|
||||
setShouldLoad(true)
|
||||
}).catch(() => {
|
||||
setImageError(true)
|
||||
}).finally(() => {
|
||||
setIsInQueue(false)
|
||||
})
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: '50px' // 减少预加载距离,只提前50px
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(containerRef.current)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [session.avatarUrl, shouldLoad, isInQueue])
|
||||
|
||||
// 当 avatarUrl 变化时重置状态
|
||||
useEffect(() => {
|
||||
setImageLoaded(false)
|
||||
setImageError(false)
|
||||
setShouldLoad(Boolean(session.avatarUrl))
|
||||
setShouldLoad(false)
|
||||
setIsInQueue(false)
|
||||
}, [session.avatarUrl])
|
||||
|
||||
const hasValidUrl = Boolean(session.avatarUrl && !imageError && shouldLoad)
|
||||
// 检查图片是否已经从缓存加载完成
|
||||
useEffect(() => {
|
||||
if (shouldLoad && imgRef.current?.complete && imgRef.current?.naturalWidth > 0) {
|
||||
setImageLoaded(true)
|
||||
}
|
||||
}, [session.avatarUrl, shouldLoad])
|
||||
|
||||
const hasValidUrl = session.avatarUrl && !imageError && shouldLoad
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -199,7 +287,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
|
||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
||||
const [hasInitialMessages, setHasInitialMessages] = useState(false)
|
||||
|
||||
|
||||
// 联系人信息加载控制
|
||||
const isEnrichingRef = useRef(false)
|
||||
const enrichCancelledRef = useRef(false)
|
||||
@@ -292,10 +380,10 @@ function ChatPage(_props: ChatPageProps) {
|
||||
// 确保 nextSessions 也是数组
|
||||
if (Array.isArray(nextSessions)) {
|
||||
setSessions(nextSessions)
|
||||
// 启动联系人信息加载,UI 已经渲染完成
|
||||
// 延迟启动联系人信息加载,确保UI先渲染完成
|
||||
setTimeout(() => {
|
||||
void enrichSessionsContactInfo(nextSessions)
|
||||
}, 0)
|
||||
}, 500)
|
||||
} else {
|
||||
console.error('mergeSessions returned non-array:', nextSessions)
|
||||
setSessions(sessionsArray)
|
||||
@@ -319,25 +407,28 @@ function ChatPage(_props: ChatPageProps) {
|
||||
// 分批异步加载联系人信息(优化性能:防止重复加载,滚动时暂停,只在空闲时加载)
|
||||
const enrichSessionsContactInfo = async (sessions: ChatSession[]) => {
|
||||
if (sessions.length === 0) return
|
||||
|
||||
|
||||
// 防止重复加载
|
||||
if (isEnrichingRef.current) {
|
||||
console.log('[性能监控] 联系人信息正在加载中,跳过重复请求')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isEnrichingRef.current = true
|
||||
enrichCancelledRef.current = false
|
||||
|
||||
|
||||
console.log(`[性能监控] 开始加载联系人信息,会话数: ${sessions.length}`)
|
||||
const totalStart = performance.now()
|
||||
|
||||
|
||||
// 延迟启动,等待UI渲染完成
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 检查是否被取消
|
||||
if (enrichCancelledRef.current) {
|
||||
isEnrichingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 找出需要加载联系人信息的会话(没有缓存的)
|
||||
const needEnrich = sessions.filter(s => !s.avatarUrl && (!s.displayName || s.displayName === s.username))
|
||||
@@ -349,10 +440,10 @@ function ChatPage(_props: ChatPageProps) {
|
||||
|
||||
console.log(`[性能监控] 需要加载的联系人信息: ${needEnrich.length} 个`)
|
||||
|
||||
// 每次最多查询更多联系人,减少批次数
|
||||
const batchSize = 20
|
||||
// 进一步减少批次大小,每批3个,避免DLL调用阻塞
|
||||
const batchSize = 3
|
||||
let loadedCount = 0
|
||||
|
||||
|
||||
for (let i = 0; i < needEnrich.length; i += batchSize) {
|
||||
// 如果正在滚动,暂停加载
|
||||
if (isScrollingRef.current) {
|
||||
@@ -363,25 +454,41 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}
|
||||
if (enrichCancelledRef.current) break
|
||||
}
|
||||
|
||||
|
||||
// 检查是否被取消
|
||||
if (enrichCancelledRef.current) break
|
||||
|
||||
|
||||
const batchStart = performance.now()
|
||||
const batch = needEnrich.slice(i, i + batchSize)
|
||||
const usernames = batch.map(s => s.username)
|
||||
|
||||
// 在执行 DLL 请求前让出控制权以保持响应
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
await loadContactInfoBatch(usernames)
|
||||
|
||||
|
||||
// 使用 requestIdleCallback 延迟执行,避免阻塞UI
|
||||
await new Promise<void>((resolve) => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(() => {
|
||||
void loadContactInfoBatch(usernames).then(() => resolve())
|
||||
}, { timeout: 2000 })
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
void loadContactInfoBatch(usernames).then(() => resolve())
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
loadedCount += batch.length
|
||||
const batchTime = performance.now() - batchStart
|
||||
if (batchTime > 200) {
|
||||
console.warn(`[性能监控] 批次 ${Math.floor(i / batchSize) + 1}/${Math.ceil(needEnrich.length / batchSize)} 耗时: ${batchTime.toFixed(2)}ms (已加载: ${loadedCount}/${needEnrich.length})`)
|
||||
}
|
||||
|
||||
// 批次间延迟,给UI更多时间(DLL调用可能阻塞,需要更长的延迟)
|
||||
if (i + batchSize < needEnrich.length && !enrichCancelledRef.current) {
|
||||
// 如果不在滚动,可以延迟短一点
|
||||
const delay = isScrollingRef.current ? 1000 : 800
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalTime = performance.now() - totalStart
|
||||
if (!enrichCancelledRef.current) {
|
||||
console.log(`[性能监控] 联系人信息加载完成,总耗时: ${totalTime.toFixed(2)}ms, 已加载: ${loadedCount}/${needEnrich.length}`)
|
||||
@@ -463,19 +570,19 @@ function ChatPage(_props: ChatPageProps) {
|
||||
try {
|
||||
// 在 DLL 调用前让出控制权(使用 setTimeout 0 代替 setImmediate)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
|
||||
const dllStart = performance.now()
|
||||
const result = await window.electronAPI.chat.enrichSessionsContactInfo(usernames)
|
||||
const dllTime = performance.now() - dllStart
|
||||
|
||||
|
||||
// DLL 调用后再次让出控制权
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
|
||||
const totalTime = performance.now() - startTime
|
||||
if (dllTime > 50 || totalTime > 100) {
|
||||
console.warn(`[性能监控] DLL调用耗时: ${dllTime.toFixed(2)}ms, 总耗时: ${totalTime.toFixed(2)}ms, usernames: ${usernames.length}`)
|
||||
}
|
||||
|
||||
|
||||
if (result.success && result.contacts) {
|
||||
// 将更新加入队列,而不是立即更新
|
||||
for (const [username, contact] of Object.entries(result.contacts)) {
|
||||
@@ -531,41 +638,16 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const loadCachedMessagesForSession = async (sessionId: string) => {
|
||||
if (!sessionId) return
|
||||
const cached = sessionMessageCache.get(sessionId)
|
||||
if (cached && cached.length > 0) {
|
||||
setMessages(cached)
|
||||
setHasInitialMessages(true)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await window.electronAPI.chat.getCachedMessages(sessionId)
|
||||
if (result.success && Array.isArray(result.messages) && result.messages.length > 0) {
|
||||
const trimmed = result.messages.length > SESSION_MESSAGE_CACHE_LIMIT
|
||||
? result.messages.slice(-SESSION_MESSAGE_CACHE_LIMIT)
|
||||
: result.messages
|
||||
sessionMessageCache.set(sessionId, trimmed)
|
||||
setMessages(trimmed)
|
||||
setHasInitialMessages(true)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存消息失败:', error)
|
||||
}
|
||||
setMessages([])
|
||||
setHasInitialMessages(false)
|
||||
}
|
||||
|
||||
// 加载消息
|
||||
const loadMessages = async (sessionId: string, offset = 0) => {
|
||||
const listEl = messageListRef.current
|
||||
const session = sessionMapRef.current.get(sessionId)
|
||||
const unreadCount = session?.unreadCount ?? 0
|
||||
const messageLimit = offset === 0 && unreadCount > 99 ? 30 : 50
|
||||
|
||||
|
||||
if (offset === 0) {
|
||||
setLoadingMessages(true)
|
||||
setMessages([])
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
@@ -578,7 +660,6 @@ function ChatPage(_props: ChatPageProps) {
|
||||
if (result.success && result.messages) {
|
||||
if (offset === 0) {
|
||||
setMessages(result.messages)
|
||||
cacheSessionMessages(sessionId, result.messages)
|
||||
// 首次加载滚动到底部
|
||||
requestAnimationFrame(() => {
|
||||
if (messageListRef.current) {
|
||||
@@ -613,18 +694,14 @@ function ChatPage(_props: ChatPageProps) {
|
||||
// 选择会话
|
||||
const handleSelectSession = (session: ChatSession) => {
|
||||
if (session.username === currentSessionId) return
|
||||
const sessionId = session.username
|
||||
setCurrentSession(sessionId)
|
||||
setCurrentSession(session.username)
|
||||
setCurrentOffset(0)
|
||||
loadMessages(session.username, 0)
|
||||
// 重置详情面板
|
||||
setSessionDetail(null)
|
||||
if (showDetailPanel) {
|
||||
loadSessionDetail(sessionId)
|
||||
loadSessionDetail(session.username)
|
||||
}
|
||||
void (async () => {
|
||||
await loadCachedMessagesForSession(sessionId)
|
||||
loadMessages(sessionId, 0)
|
||||
})()
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
@@ -665,7 +742,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
|
||||
scrollTimeoutRef.current = requestAnimationFrame(() => {
|
||||
if (!messageListRef.current) return
|
||||
|
||||
|
||||
const { scrollTop, clientHeight, scrollHeight } = messageListRef.current
|
||||
|
||||
// 显示回到底部按钮:距离底部超过 300px
|
||||
@@ -765,9 +842,10 @@ function ChatPage(_props: ChatPageProps) {
|
||||
if (!isConnected && !isConnecting) {
|
||||
connect()
|
||||
}
|
||||
|
||||
|
||||
// 组件卸载时清理
|
||||
return () => {
|
||||
avatarLoadQueue.clear()
|
||||
if (contactUpdateTimerRef.current) {
|
||||
clearTimeout(contactUpdateTimerRef.current)
|
||||
}
|
||||
@@ -828,7 +906,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
})
|
||||
}
|
||||
if (payloads.length > 0) {
|
||||
window.electronAPI.image.preload(payloads).catch(() => {})
|
||||
window.electronAPI.image.preload(payloads).catch(() => { })
|
||||
}
|
||||
}, [currentSessionId, messages])
|
||||
|
||||
@@ -1036,7 +1114,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
))}
|
||||
</div>
|
||||
) : Array.isArray(filteredSessions) && filteredSessions.length > 0 ? (
|
||||
<div
|
||||
<div
|
||||
className="session-list"
|
||||
ref={sessionListRef}
|
||||
onScroll={() => {
|
||||
@@ -1117,56 +1195,56 @@ function ChatPage(_props: ChatPageProps) {
|
||||
ref={messageListRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{hasMoreMessages && (
|
||||
<div className={`load-more-trigger ${isLoadingMore ? 'loading' : ''}`}>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<Loader2 size={14} />
|
||||
<span>加载更多...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>向上滚动加载更多</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
const prevMsg = index > 0 ? messages[index - 1] : undefined
|
||||
const showDateDivider = shouldShowDateDivider(msg, prevMsg)
|
||||
|
||||
// 显示时间:第一条消息,或者与上一条消息间隔超过5分钟
|
||||
const showTime = !prevMsg || (msg.createTime - prevMsg.createTime > 300)
|
||||
const isSent = msg.isSend === 1
|
||||
const isSystem = msg.localType === 10000
|
||||
|
||||
// 系统消息居中显示
|
||||
const wrapperClass = isSystem ? 'system' : (isSent ? 'sent' : 'received')
|
||||
|
||||
const messageKey = getMessageKey(msg)
|
||||
return (
|
||||
<div key={messageKey} className={`message-wrapper ${wrapperClass} ${highlightedMessageSet.has(messageKey) ? 'new-message' : ''}`}>
|
||||
{showDateDivider && (
|
||||
<div className="date-divider">
|
||||
<span>{formatDateDivider(msg.createTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
<MessageBubble
|
||||
message={msg}
|
||||
session={currentSession}
|
||||
showTime={!showDateDivider && showTime}
|
||||
myAvatarUrl={myAvatarUrl}
|
||||
isGroupChat={isGroupChat(currentSession.username)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* 回到底部按钮 */}
|
||||
<div className={`scroll-to-bottom ${showScrollToBottom ? 'show' : ''}`} onClick={scrollToBottom}>
|
||||
<ChevronDown size={16} />
|
||||
<span>回到底部</span>
|
||||
{hasMoreMessages && (
|
||||
<div className={`load-more-trigger ${isLoadingMore ? 'loading' : ''}`}>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<Loader2 size={14} />
|
||||
<span>加载更多...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>向上滚动加载更多</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
const prevMsg = index > 0 ? messages[index - 1] : undefined
|
||||
const showDateDivider = shouldShowDateDivider(msg, prevMsg)
|
||||
|
||||
// 显示时间:第一条消息,或者与上一条消息间隔超过5分钟
|
||||
const showTime = !prevMsg || (msg.createTime - prevMsg.createTime > 300)
|
||||
const isSent = msg.isSend === 1
|
||||
const isSystem = msg.localType === 10000
|
||||
|
||||
// 系统消息居中显示
|
||||
const wrapperClass = isSystem ? 'system' : (isSent ? 'sent' : 'received')
|
||||
|
||||
const messageKey = getMessageKey(msg)
|
||||
return (
|
||||
<div key={messageKey} className={`message-wrapper ${wrapperClass} ${highlightedMessageSet.has(messageKey) ? 'new-message' : ''}`}>
|
||||
{showDateDivider && (
|
||||
<div className="date-divider">
|
||||
<span>{formatDateDivider(msg.createTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
<MessageBubble
|
||||
message={msg}
|
||||
session={currentSession}
|
||||
showTime={!showDateDivider && showTime}
|
||||
myAvatarUrl={myAvatarUrl}
|
||||
isGroupChat={isGroupChat(currentSession.username)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* 回到底部按钮 */}
|
||||
<div className={`scroll-to-bottom ${showScrollToBottom ? 'show' : ''}`} onClick={scrollToBottom}>
|
||||
<ChevronDown size={16} />
|
||||
<span>回到底部</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 会话详情面板 */}
|
||||
{showDetailPanel && (
|
||||
@@ -1356,7 +1434,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
|
||||
return 'image/webp'
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
return 'image/jpeg'
|
||||
}, [])
|
||||
|
||||
@@ -1423,7 +1501,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
setSenderAvatarUrl(result.avatarUrl)
|
||||
setSenderName(result.displayName)
|
||||
}
|
||||
}).catch(() => {}).finally(() => {
|
||||
}).catch(() => { }).finally(() => {
|
||||
senderAvatarLoading.delete(sender)
|
||||
})
|
||||
}
|
||||
@@ -1519,7 +1597,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
}
|
||||
setImageHasUpdate(Boolean(result.hasUpdate))
|
||||
}
|
||||
}).catch(() => {})
|
||||
}).catch(() => { })
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
@@ -1607,6 +1685,12 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
// 是否有引用消息
|
||||
const hasQuote = message.quotedContent && message.quotedContent.length > 0
|
||||
|
||||
// 去除企业微信 ID 前缀
|
||||
const cleanMessageContent = (content: string) => {
|
||||
if (!content) return ''
|
||||
return content.replace(/^[a-zA-Z0-9]+@openim:\n?/, '')
|
||||
}
|
||||
|
||||
// 解析混合文本和表情
|
||||
const renderTextWithEmoji = (text: string) => {
|
||||
if (!text) return text
|
||||
@@ -1817,14 +1901,14 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
<div className="bubble-content">
|
||||
<div className="quoted-message">
|
||||
{message.quotedSender && <span className="quoted-sender">{message.quotedSender}</span>}
|
||||
<span className="quoted-text">{renderTextWithEmoji(message.quotedContent || '')}</span>
|
||||
<span className="quoted-text">{renderTextWithEmoji(cleanMessageContent(message.quotedContent || ''))}</span>
|
||||
</div>
|
||||
<div className="message-text">{renderTextWithEmoji(message.parsedContent)}</div>
|
||||
<div className="message-text">{renderTextWithEmoji(cleanMessageContent(message.parsedContent))}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// 普通消息
|
||||
return <div className="bubble-content">{renderTextWithEmoji(message.parsedContent)}</div>
|
||||
return <div className="bubble-content">{renderTextWithEmoji(cleanMessageContent(message.parsedContent))}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -506,6 +506,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
{dbKeyStatus && <div className="field-hint status-text">{dbKeyStatus}</div>}
|
||||
<div className="field-hint">获取密钥会自动识别最近登录的账号</div>
|
||||
<div className="field-hint">如果获取秘钥失败 请在微信打开后等待10秒后再登录</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user