Merge: 解决冲突 - 保留链接消息和视频消息样式,合并 rawContent 和 content 字段

This commit is contained in:
Forrest
2026-01-18 23:25:28 +08:00
11 changed files with 1224 additions and 118 deletions

View File

@@ -1343,6 +1343,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
const isSystem = isSystemMessage(message.localType)
const isEmoji = message.localType === 47
const isImage = message.localType === 3
const isVideo = message.localType === 43
const isVoice = message.localType === 34
const isSent = message.isSend === 1
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
@@ -1371,6 +1372,56 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
const [voiceWaveform, setVoiceWaveform] = useState<number[]>([])
const voiceAutoDecryptTriggered = useRef(false)
// 视频相关状态
const [videoLoading, setVideoLoading] = useState(false)
const [videoInfo, setVideoInfo] = useState<{ videoUrl?: string; coverUrl?: string; thumbUrl?: string; exists: boolean } | null>(null)
const videoContainerRef = useRef<HTMLDivElement>(null)
const [isVideoVisible, setIsVideoVisible] = useState(false)
const [videoMd5, setVideoMd5] = useState<string | null>(null)
// 解析视频 MD5
useEffect(() => {
if (!isVideo) return
console.log('[Video Debug] Full message object:', JSON.stringify(message, null, 2))
console.log('[Video Debug] Message keys:', Object.keys(message))
console.log('[Video Debug] Message:', {
localId: message.localId,
localType: message.localType,
hasVideoMd5: !!message.videoMd5,
hasContent: !!message.content,
hasParsedContent: !!message.parsedContent,
hasRawContent: !!(message as any).rawContent,
contentPreview: message.content?.substring(0, 200),
parsedContentPreview: message.parsedContent?.substring(0, 200),
rawContentPreview: (message as any).rawContent?.substring(0, 200)
})
// 优先使用数据库中的 videoMd5
if (message.videoMd5) {
console.log('[Video Debug] Using videoMd5 from message:', message.videoMd5)
setVideoMd5(message.videoMd5)
return
}
// 尝试从多个可能的字段获取原始内容
const contentToUse = message.content || (message as any).rawContent || message.parsedContent
if (contentToUse) {
console.log('[Video Debug] Parsing MD5 from content, length:', contentToUse.length)
window.electronAPI.video.parseVideoMd5(contentToUse).then((result) => {
console.log('[Video Debug] Parse result:', result)
if (result && result.success && result.md5) {
console.log('[Video Debug] Parsed MD5:', result.md5)
setVideoMd5(result.md5)
} else {
console.error('[Video Debug] Failed to parse MD5:', result)
}
}).catch((err) => {
console.error('[Video Debug] Parse error:', err)
})
}
}, [isVideo, message.videoMd5, message.content, message.parsedContent])
// 加载自动转文字配置
useEffect(() => {
const loadConfig = async () => {
@@ -1838,6 +1889,62 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
}
}, [isVoice, message.localId, requestVoiceTranscript])
// 视频懒加载
useEffect(() => {
if (!isVideo || !videoContainerRef.current) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVideoVisible(true)
observer.disconnect()
}
})
},
{
rootMargin: '200px 0px',
threshold: 0
}
)
observer.observe(videoContainerRef.current)
return () => observer.disconnect()
}, [isVideo])
// 加载视频信息
useEffect(() => {
if (!isVideo || !isVideoVisible || videoInfo || videoLoading) return
if (!videoMd5) {
console.log('[Video Debug] No videoMd5 available yet')
return
}
console.log('[Video Debug] Loading video info for MD5:', videoMd5)
setVideoLoading(true)
window.electronAPI.video.getVideoInfo(videoMd5).then((result) => {
console.log('[Video Debug] getVideoInfo result:', result)
if (result && result.success) {
setVideoInfo({
exists: result.exists,
videoUrl: result.videoUrl,
coverUrl: result.coverUrl,
thumbUrl: result.thumbUrl
})
} else {
console.error('[Video Debug] Video info failed:', result)
setVideoInfo({ exists: false })
}
}).catch((err) => {
console.error('[Video Debug] getVideoInfo error:', err)
setVideoInfo({ exists: false })
}).finally(() => {
setVideoLoading(false)
})
}, [isVideo, isVideoVisible, videoInfo, videoLoading, videoMd5])
// 根据设置决定是否自动转写
const [autoTranscribeEnabled, setAutoTranscribeEnabled] = useState(false)
@@ -1973,6 +2080,72 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
)
}
// 视频消息
if (isVideo) {
const handlePlayVideo = useCallback(async () => {
if (!videoInfo?.videoUrl) return
try {
await window.electronAPI.window.openVideoPlayerWindow(videoInfo.videoUrl)
} catch (e) {
console.error('打开视频播放窗口失败:', e)
}
}, [videoInfo?.videoUrl])
// 未进入可视区域时显示占位符
if (!isVideoVisible) {
return (
<div className="video-placeholder" ref={videoContainerRef}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
</div>
)
}
// 加载中
if (videoLoading) {
return (
<div className="video-loading" ref={videoContainerRef}>
<Loader2 size={20} className="spin" />
</div>
)
}
// 视频不存在
if (!videoInfo?.exists || !videoInfo.videoUrl) {
return (
<div className="video-unavailable" ref={videoContainerRef}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
<span></span>
</div>
)
}
// 默认显示缩略图,点击打开独立播放窗口
const thumbSrc = videoInfo.thumbUrl || videoInfo.coverUrl
return (
<div className="video-thumb-wrapper" ref={videoContainerRef} onClick={handlePlayVideo}>
{thumbSrc ? (
<img src={thumbSrc} alt="视频缩略图" className="video-thumb" />
) : (
<div className="video-thumb-placeholder">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
</div>
)}
<div className="video-play-button">
<Play size={32} fill="white" />
</div>
</div>
)
}
if (isVoice) {
const durationText = message.voiceDurationSeconds ? `${message.voiceDurationSeconds}"` : ''
const handleToggle = async () => {