()
+ for (const message of [...recovered, ...current]) {
+ const recoveredRow = Boolean(message.raw?.['_wxe_recovered'])
+ const identity = message.mesLocalID
+ ? `local:${message.mesLocalID}`
+ : message.serverId
+ ? `server:${message.serverId}`
+ : `${message.msgCreateTime}:${message.msgContent}`
+ const key = recoveredRow ? `recovered:${identity}` : identity
+ merged.set(key, message)
+ }
+ const messages = Array.from(merged.values()).sort(
+ (left, right) =>
+ Number(left.msgCreateTime || 0) - Number(right.msgCreateTime || 0) ||
+ Number(left.mesLocalID || 0) - Number(right.mesLocalID || 0)
+ )
+ return limit && messages.length > limit ? messages.slice(-limit) : messages
+ }
+
+ private recallJournalTableName(messageTableName: string): string {
+ return `_wxe_recall_journal_${crypto
+ .createHash('sha1')
+ .update(messageTableName)
+ .digest('hex')
+ .slice(0, 16)}`
+ }
+
getMyAvatarUrl(): string | undefined {
const candidates = this.getMyUsernameCandidates()
this.hydrateAvatarUrls(candidates)
@@ -856,26 +1028,25 @@ export class Wcdb4Client {
const visibleMessages = limit && sorted.length > limit ? sorted.slice(-limit) : sorted
- return visibleMessages
- .map((message) => {
- if (!message.sender) return message
- const senderNickname = this.displayNameCache.get(message.sender) || message.senderNickname
- const senderAvatar = this.avatarCache.get(message.sender) || message.senderAvatar
- const shouldPrefixSender =
- username.endsWith('@chatroom') &&
- message.mesDes === 1 &&
- message.sender &&
- message.msgContent &&
- !message.msgContent.startsWith(`${message.sender}:`)
- return {
- ...message,
- senderNickname,
- senderAvatar,
- msgContent: shouldPrefixSender
- ? `${message.sender}:\n${message.msgContent}`
- : message.msgContent
- }
- })
+ return visibleMessages.map((message) => {
+ if (!message.sender) return message
+ const senderNickname = this.displayNameCache.get(message.sender) || message.senderNickname
+ const senderAvatar = this.avatarCache.get(message.sender) || message.senderAvatar
+ const shouldPrefixSender =
+ username.endsWith('@chatroom') &&
+ message.mesDes === 1 &&
+ message.sender &&
+ message.msgContent &&
+ !message.msgContent.startsWith(`${message.sender}:`)
+ return {
+ ...message,
+ senderNickname,
+ senderAvatar,
+ msgContent: shouldPrefixSender
+ ? `${message.sender}:\n${message.msgContent}`
+ : message.msgContent
+ }
+ })
}
private normalizeMessageLimit(limit?: number): number | undefined {
@@ -915,11 +1086,7 @@ export class Wcdb4Client {
'contactRemark',
'contact_remark'
])
- const memberNickname = this.pickString(row, [
- 'displayName',
- 'display_name',
- 'name'
- ])
+ const memberNickname = this.pickString(row, ['displayName', 'display_name', 'name'])
const avatar = this.pickString(row, [
'avatarUrl',
'avatar_url',
@@ -933,8 +1100,7 @@ export class Wcdb4Client {
return {
m_nsUsrName: username,
- nickname:
- groupNicknames.get(username) || remark || wechatNickname || memberNickname,
+ nickname: groupNicknames.get(username) || remark || wechatNickname || memberNickname,
groupNickname: groupNicknames.get(username) || '',
wechatNickname: wechatNickname || memberNickname,
remark,
@@ -1520,6 +1686,19 @@ export class Wcdb4Client {
'mesLocalID',
'id'
])
+ const serverId = this.pickString(row, [
+ 'server_id',
+ 'serverId',
+ 'svr_id',
+ 'svrId',
+ 'msg_svr_id',
+ 'msgSvrId',
+ 'message_id',
+ 'messageId',
+ 'new_msg_id',
+ 'newMsgId',
+ 'WCDB_CT_server_id'
+ ])
const messageType = this.pickString(row, [
'local_type',
'localType',
@@ -1541,6 +1720,7 @@ export class Wcdb4Client {
return {
mesLocalID: localId || `${createTime}-${this.md5(JSON.stringify(row))}`,
+ serverId: serverId || undefined,
mesDes: isSend ? 0 : 1,
messageType: messageType || '1',
msgCreateTime: String(createTime),
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 393d89f..18ca3e7 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -1296,6 +1296,15 @@ body {
font-size: 12px;
}
+.message-recalled-status {
+ align-self: flex-end;
+ margin-top: 3px;
+ color: #c63c32;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 16px;
+}
+
.message-hover-time {
position: absolute;
top: 50%;
diff --git a/src/renderer/src/components/chat/MessageBubble.tsx b/src/renderer/src/components/chat/MessageBubble.tsx
index 9d5df9c..30d6dab 100644
--- a/src/renderer/src/components/chat/MessageBubble.tsx
+++ b/src/renderer/src/components/chat/MessageBubble.tsx
@@ -56,6 +56,11 @@ export function MessageBubble({
{renderWechatEmojiText(message.content)}
)}
+ {message.recalled && (
+
+ 消息已撤回
+
+ )}
{hoverTime}
{!isGroupChat && !isMine && contact.m_nsNickName && (
{contact.m_nsNickName}
diff --git a/src/shared/types.ts b/src/shared/types.ts
index 2bf8c61..0c6ab26 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -20,8 +20,12 @@ export interface Message {
voiceDataUrl?: string
voiceDuration?: number
localId?: number
+ serverId?: string
createTime?: number
sessionId?: string
+ recalled?: boolean
+ recalledBy?: string
+ recoveredFromRecallJournal?: boolean
}
type TextContent = { type: 'text'; content: string }
@@ -67,7 +71,19 @@ type QuoteContent = {
quotedSender?: string
quotedType?: string
}
-type SystemContent = { type: 'system'; content: string; raw?: string }
+type SystemContent = {
+ type: 'system'
+ content: string
+ raw?: string
+ recall?: {
+ targetId?: string
+ targetIds?: string[]
+ replacement: string
+ actor?: string
+ sessionId?: string
+ recallTime?: number
+ }
+}
type UnknownContent = { type: 'unknown'; raw: string }
export type ParsedContent =