♻️ refactor(im):注释对齐 + patchMessage 复用 applyServerMessageUpdate
- recomputeConversationLast / videoCoverUrl / showSendingLoading 三处 JSDoc 跟实现对齐: 原描述还停留在旧设计(lastSendTime 不重算 / 占位 coverUrl 用 blob / 「外层 loading 多余」), 这轮一并改成当前事实,避免后续维护被误导 - patchMessage 删手写 revoke + Object.assign,改调 applyServerMessageUpdate, 与 ackMessage / insertMessage(existingIndex) 共用一份服务端字段合并语义; 「值未变早返回」保留在 patchMessage 顶部 - 抽 BLOB_URL_PREFIX 常量替代散落在 utils/message.ts 与 useMediaUploader.ts 的 3 处 'blob:' 字面量im
parent
30d695d702
commit
1ac0650984
|
|
@ -7,6 +7,7 @@ import { useMuteOverlay } from './useMuteOverlay'
|
|||
import { ImMessageStatus, ImMessageType } from '../../utils/constants'
|
||||
import { getConversationKey } from '../../utils/conversation'
|
||||
import {
|
||||
BLOB_URL_PREFIX,
|
||||
generateClientMessageId,
|
||||
parseMessage,
|
||||
serializeMessage,
|
||||
|
|
@ -27,7 +28,7 @@ export type MediaPayload = ImageMessage | FileMessage | AudioMessage | VideoMess
|
|||
*
|
||||
* - voiceDuration:语音时长(秒),首发由 VoiceRecorder 给,重传从旧 AudioMessage.duration 取
|
||||
* - videoProbe:视频元信息(首发由 probeVideoFile 解出,重传从旧 VideoMessage 直接拷字段)
|
||||
* - videoCoverUrl:视频封面真实 URL;占位阶段用 blob,commit 用真实 URL,重传时旧值若是 blob 会被跳过
|
||||
* - videoCoverUrl:视频封面真实 URL;占位阶段不设(避免传 blob 当 poster 在部分浏览器退化),commit 阶段由 cover 上传结果填入;重传时从旧 VideoMessage.coverUrl 复用,旧值若是 blob 会被跳过
|
||||
*/
|
||||
export interface MediaTypeContext {
|
||||
voiceDuration?: number
|
||||
|
|
@ -79,7 +80,7 @@ export const mediaTypeHandlers: Partial<Record<number, MediaTypeHandler>> = {
|
|||
extractResendContext: (oldContent) => {
|
||||
const old = parseMessage<VideoMessage>(oldContent)
|
||||
// 旧 coverUrl 是 blob 说明上传期失败(cover 没传成功),不复用;真实 URL 直接复用,省一次封面上传
|
||||
const reuseCover = old?.coverUrl && !old.coverUrl.startsWith('blob:') ? old.coverUrl : undefined
|
||||
const reuseCover = old?.coverUrl && !old.coverUrl.startsWith(BLOB_URL_PREFIX) ? old.coverUrl : undefined
|
||||
return {
|
||||
videoProbe: { duration: old?.duration, width: old?.width, height: old?.height },
|
||||
videoCoverUrl: reuseCover
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ const uploadProgressText = computed(() => `${uploadProgress.value}%`)
|
|||
/**
|
||||
* 是否在气泡尾部显示「发送中」loading 转圈
|
||||
*
|
||||
* 图片 / 视频 / 文件气泡内嵌已有进度反馈(遮罩 / 进度条),外层 loading 多余则抑制;
|
||||
* 图片 / 视频 / 文件气泡内嵌已有进度反馈(遮罩 / 进度条),外层 loading 不再叠加;
|
||||
* 语音气泡只有麦克风 + 时长,无内嵌进度,必须保留外层 loading 让用户感知正在发送
|
||||
*/
|
||||
const showSendingLoading = computed(
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ function deriveLastSenderDisplayName(
|
|||
/**
|
||||
* 按 conversation.messages 末尾重算 last* 系列摘要 / 事实索引
|
||||
*
|
||||
* 用于:删除最后一条消息 / loadConversations drop 媒体占位后;剩余消息为空则字段一并清空。
|
||||
* 不重算 lastSendTime 兜底(保留原 conversation 现值),与 removeMessage 旧行为一致
|
||||
* 用于:删除最后一条消息 / loadConversations drop 媒体占位后;剩余消息为空则字段一并清空(含 lastSendTime=0),让空会话排到列表末尾。
|
||||
* 末条消息存在时,lastSendTime 取该消息的 sendTime;缺失时沿用 conversation 现值
|
||||
*/
|
||||
function recomputeConversationLast(conversation: Conversation): void {
|
||||
const last = conversation.messages[conversation.messages.length - 1]
|
||||
|
|
@ -627,11 +627,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
if (!changed) {
|
||||
return
|
||||
}
|
||||
// 替换 content 时 revoke 旧 blob URL,与 ackMessage 同语义
|
||||
if (patch.content && patch.content !== message.content) {
|
||||
revokeBlobUrlsInContent(message.content)
|
||||
}
|
||||
Object.assign(message, patch)
|
||||
applyServerMessageUpdate(message, patch)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -105,12 +105,15 @@ export const parseMessage = <T>(content: string): T | null => {
|
|||
/** 序列化消息 payload 为 content JSON 字符串;与 parseMessage 对称 */
|
||||
export const serializeMessage = <T>(payload: T): string => JSON.stringify(payload)
|
||||
|
||||
/** `URL.createObjectURL(file)` 生成的 URL 前缀;占位 / revoke / 重传旧值识别共用 */
|
||||
export const BLOB_URL_PREFIX = 'blob:'
|
||||
|
||||
/**
|
||||
* 媒体 payload 里可能包含 blob URL 的字段(图片/文件/视频/语音都对齐这套 url 字段命名)
|
||||
*
|
||||
* 跟随 ImageMessage / VideoMessage / FileMessage / AudioMessage interface 定义同步:
|
||||
* - url:主体资源(占位时是 blob URL,ack 后是真实 URL)
|
||||
* - coverUrl:视频封面(占位时跟 url 同 blob,cover 上传成功后是真实 URL)
|
||||
* - coverUrl:视频封面(commit 后是真实 URL;占位阶段不设以避免传 blob 当 poster 在部分浏览器退化)
|
||||
* - thumbnailUrl:图片缩略图(当前未占位时使用 blob,预留)
|
||||
*/
|
||||
const MEDIA_BLOB_URL_FIELDS = ['url', 'coverUrl', 'thumbnailUrl'] as const
|
||||
|
|
@ -122,7 +125,7 @@ const MEDIA_BLOB_URL_FIELDS = ['url', 'coverUrl', 'thumbnailUrl'] as const
|
|||
* 仅对当前 document 内创建的 blob URL 有效;IndexedDB 恢复出来的旧 blob URL 已随旧 document 失效,调它无害但无意义
|
||||
*/
|
||||
export const revokeBlobUrlsInContent = (content: string): void => {
|
||||
if (!content || !content.includes('blob:')) {
|
||||
if (!content || !content.includes(BLOB_URL_PREFIX)) {
|
||||
return
|
||||
}
|
||||
const payload = parseMessage<Record<string, unknown>>(content)
|
||||
|
|
@ -131,7 +134,7 @@ export const revokeBlobUrlsInContent = (content: string): void => {
|
|||
}
|
||||
for (const field of MEDIA_BLOB_URL_FIELDS) {
|
||||
const value = payload[field]
|
||||
if (typeof value === 'string' && value.startsWith('blob:')) {
|
||||
if (typeof value === 'string' && value.startsWith(BLOB_URL_PREFIX)) {
|
||||
URL.revokeObjectURL(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue