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