From a2017be96abd5b28f01b610d884e985782caf0a2 Mon Sep 17 00:00:00 2001
From: bob <312777916@qq.com>
Date: Sat, 17 May 2025 12:21:50 +0800
Subject: [PATCH] =?UTF-8?q?msg.content=E6=B6=88=E6=81=AF=E7=BB=93=E6=9E=84?=
=?UTF-8?q?=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/const/msgConst.js | 26 +-
src/js/utils/common.js | 10 +
src/js/utils/message.js | 188 +++++++
src/js/websocket/constructor.js | 6 +-
src/js/websocket/wsConnect.js | 10 +-
src/models/message.js | 2 +
src/proto/msg.js | 46 +-
src/proto/msg.proto | 15 +-
src/stores/audio.js | 22 +-
src/stores/document.js | 19 +-
src/stores/image.js | 54 +-
src/stores/message.js | 8 +-
src/stores/video.js | 19 +-
src/views/message/MessageLayout.vue | 122 ++--
.../message/components/DialogForMsgList.vue | 369 +++++-------
src/views/message/components/InputEditor.vue | 528 +++++++++++-------
.../message/components/InputRecorder.vue | 14 +-
src/views/message/components/InputToolBar.vue | 6 +-
src/views/message/components/MenuAddOpr.vue | 1 +
src/views/message/components/MenuMsgItem.vue | 19 +-
src/views/message/components/MenuMsgMain.vue | 1 +
src/views/message/components/MenuSession.vue | 1 +
src/views/message/components/MessageItem.vue | 404 ++++++--------
src/views/message/components/MsgBoxImage.vue | 10 +-
.../message/components/MsgBoxRecording.vue | 16 +-
src/views/message/components/SessionItem.vue | 112 +---
26 files changed, 1080 insertions(+), 948 deletions(-)
create mode 100644 src/js/utils/message.js
diff --git a/src/const/msgConst.js b/src/const/msgConst.js
index ecaf536..4b5f173 100644
--- a/src/const/msgConst.js
+++ b/src/const/msgConst.js
@@ -11,17 +11,23 @@ export const proto = {
// 和服务端约定好的,第一个消息都是从10001开始的
export const BEGIN_MSG_ID = 10001
-// 消息内容类型
+/**
+ * 消息内容类型
+ * MIX类型为TEXT,EMOJI,SCREENSHOT,AT,QUOTE的组合
+ */
export const msgContentType = {
- MIX: 0, // 组合,包含多种类型
- TEXT: 1, // 文本
- IMAGE: 2, // 图片
- RECORDING: 3, // 语音
- AUDIO: 4, // 音频文件
- EMOJI: 5, // 表情
- VIDEO: 6, // 视频
- DOCUMENT: 7, // 文档
- FORWARD_TOGETHER: 10 // 合并转发消息
+ TEXT: 0b0000000000000001, // 文本
+ EMOJI: 0b0000000000000010, // 表情
+ SCREENSHOT: 0b0000000000000100, // 截图
+ AT: 0b0000000000001000, // @
+ QUOTE: 0b0000000000010000, // 引用
+
+ IMAGE: 0b0000001000000000, // 图片
+ RECORDING: 0b0000010000000000, // 语音
+ AUDIO: 0b0000100000000000, // 音频文件
+ VIDEO: 0b0001000000000000, // 视频
+ DOCUMENT: 0b0010000000000000, // 文档
+ FORWARD_TOGETHER: 0b0100000000000000 // 合并转发消息
}
// 消息发送状态
diff --git a/src/js/utils/common.js b/src/js/utils/common.js
index bb2f35f..bab28ca 100644
--- a/src/js/utils/common.js
+++ b/src/js/utils/common.js
@@ -164,6 +164,16 @@ export const showTimeFormatDay = (datatime) => {
return `${year}-${month}-${day}`
}
+export const showDurationFormat = (duration) => {
+ if (!duration) {
+ return '0:00'
+ }
+
+ const minutes = Math.floor(duration / 60)
+ const seconds = Math.floor(duration % 60)
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
+}
+
export const combineId = (fromId, toId) => {
if (fromId < toId) {
return fromId + '@' + toId
diff --git a/src/js/utils/message.js b/src/js/utils/message.js
new file mode 100644
index 0000000..61a96cb
--- /dev/null
+++ b/src/js/utils/message.js
@@ -0,0 +1,188 @@
+import { msgContentType } from '@/const/msgConst'
+import { jsonParseSafe, showDurationFormat } from './common'
+import { useImageStore, useAudioStore, useVideoStore, useDocumentStore } from '@/stores'
+import { emojis } from './emojis'
+
+const imageData = useImageStore()
+const audioData = useAudioStore()
+const videoData = useVideoStore()
+const documentData = useDocumentStore()
+
+export const showSimplifyMsgContent = (content) => {
+ const arr = jsonParseSafe(content)
+ if (!arr) {
+ return content
+ }
+
+ let simplifyContent = ''
+
+ for (const item of arr) {
+ if (!item.type || !item.value) {
+ return content
+ }
+
+ switch (item.type) {
+ case msgContentType.TEXT:
+ case msgContentType.EMOJI:
+ simplifyContent = simplifyContent + item.value
+ break
+ case msgContentType.AT:
+ simplifyContent = simplifyContent + `@${item.value.nickName} `
+ break
+ case msgContentType.SCREENSHOT:
+ simplifyContent = simplifyContent + `[截图]`
+ break
+ case msgContentType.QUOTE:
+ simplifyContent = simplifyContent + '[引用]'
+ break
+ case msgContentType.RECORDING:
+ simplifyContent =
+ simplifyContent + `[语音] ${showDurationFormat(audioData.audio[item.value].duration)}`
+ break
+ case msgContentType.IMAGE:
+ simplifyContent = simplifyContent + `[图片] ${imageData.image[item.value].fileName}`
+ break
+ case msgContentType.AUDIO:
+ simplifyContent = simplifyContent + `[音频] ${audioData.audio[item.value].fileName}`
+ break
+ case msgContentType.VIDEO:
+ simplifyContent = simplifyContent + `[视频] ${videoData.video[item.value].fileName}`
+ break
+ case msgContentType.DOCUMENT:
+ simplifyContent = simplifyContent + `[文件] ${documentData.document[item.value].fileName}`
+ break
+ case msgContentType.FORWARD_TOGETHER:
+ simplifyContent = simplifyContent + '[聊天记录]'
+ break
+
+ default:
+ simplifyContent = simplifyContent + item.value
+ break
+ }
+ }
+ return simplifyContent
+}
+
+/**
+ * 内容字符串是否匹配消息结构
+ */
+export const isMatchMsgStruct = (contentStr) => {
+ const contentArr = jsonParseSafe(contentStr)
+ if (!contentArr || !Array.isArray(contentArr) || contentArr.length === 0) {
+ return false
+ }
+
+ for (const item of contentArr) {
+ const { type, value } = item
+ if (!type || !value) {
+ return false
+ }
+
+ switch (type) {
+ case msgContentType.TEXT:
+ break
+ case msgContentType.EMOJI:
+ if (!(value in emojis)) {
+ return false
+ }
+ break
+ case msgContentType.SCREENSHOT:
+ case msgContentType.IMAGE:
+ case msgContentType.RECORDING:
+ case msgContentType.AUDIO:
+ case msgContentType.VIDEO:
+ case msgContentType.DOCUMENT:
+ if (!/^\d+$/.test(value)) {
+ return false
+ }
+ break
+ case msgContentType.AT: {
+ const { account, nickName } = value
+ if (!account || !nickName) {
+ return false
+ }
+ break
+ }
+ case msgContentType.QUOTE: {
+ const { msgId, nickName } = value
+ if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
+ return false
+ }
+ break
+ }
+ case msgContentType.FORWARD_TOGETHER: {
+ const { sessionId, data } = value
+ if (!sessionId || !data) {
+ return false
+ }
+
+ if (Array.isArray(data)) {
+ return
+ }
+
+ for (const item of data) {
+ const { msgId, nickName } = item
+ if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
+ return false
+ }
+ }
+ break
+ }
+ default:
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * 是否为 MIX 类型
+ * @param {*} type
+ * @returns
+ */
+export const isMixType = (type) => {
+ const MIX_CANDIDATES =
+ msgContentType.TEXT |
+ msgContentType.EMOJI |
+ msgContentType.SCREENSHOT |
+ msgContentType.AT |
+ msgContentType.QUOTE
+
+ return type <= MIX_CANDIDATES
+}
+
+/**
+ * 所有包含图片的type集合
+ * @returns
+ */
+export const imageTypes = () => {
+ return [
+ msgContentType.IMAGE,
+ msgContentType.SCREENSHOT,
+ msgContentType.SCREENSHOT || msgContentType.TEXT,
+ msgContentType.SCREENSHOT || msgContentType.EMOJI,
+ msgContentType.SCREENSHOT || msgContentType.AT,
+ msgContentType.SCREENSHOT || msgContentType.QUOTE,
+ msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI,
+ msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT,
+ msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.QUOTE,
+ msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT,
+ msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.QUOTE,
+ msgContentType.SCREENSHOT || msgContentType.AT || msgContentType.QUOTE,
+ msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI || msgContentType.AT,
+ msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT || msgContentType.QUOTE,
+ msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT || msgContentType.QUOTE,
+
+ msgContentType.SCREENSHOT ||
+ msgContentType.TEXT ||
+ msgContentType.EMOJI ||
+ msgContentType.QUOTE,
+
+ msgContentType.SCREENSHOT ||
+ msgContentType.TEXT ||
+ msgContentType.EMOJI ||
+ msgContentType.AT ||
+ msgContentType.QUOTE
+ ]
+}
diff --git a/src/js/websocket/constructor.js b/src/js/websocket/constructor.js
index 0f8d650..3650345 100644
--- a/src/js/websocket/constructor.js
+++ b/src/js/websocket/constructor.js
@@ -3,7 +3,7 @@ import { proto } from '@/const/msgConst'
import { useUserStore } from '@/stores'
import { v4 as uuidv4 } from 'uuid'
-export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
+export const chatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
const header = Header.create({
magic: proto.magic,
version: proto.version,
@@ -18,6 +18,7 @@ export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
toId: remoteId,
sessionId: sessionId,
content: content,
+ contentType: contentType,
seq: sequence
})
const chatMsg = Msg.create({ header: header, body: body })
@@ -27,7 +28,7 @@ export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
return data
}
-export const groupChatConstructor = ({ sessionId, remoteId, content, sequence }) => {
+export const groupChatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
const header = Header.create({
magic: proto.magic,
version: proto.version,
@@ -42,6 +43,7 @@ export const groupChatConstructor = ({ sessionId, remoteId, content, sequence })
sessionId: sessionId,
groupId: remoteId,
content: content,
+ contentType: contentType,
seq: sequence
})
const msg = Msg.create({ header: header, body: body })
diff --git a/src/js/websocket/wsConnect.js b/src/js/websocket/wsConnect.js
index 7d09f7b..6e87458 100644
--- a/src/js/websocket/wsConnect.js
+++ b/src/js/websocket/wsConnect.js
@@ -343,9 +343,15 @@ class WsConnect {
* @param {*} before 发送前的处理,用于展示发送前状态
* @param {*} after 发送后(接收MsgType.DELIVERED时)的处理,用于展示发送后状态
*/
- sendMsg(sessionId, remoteId, msgType, content, seq, before, after) {
+ sendMsg(sessionId, remoteId, msgType, content, contentType, seq, before, after) {
const sequence = seq || uuidv4()
- const data = this.dataConstructor[msgType]({ sessionId, remoteId, content, sequence })
+ const data = this.dataConstructor[msgType]({
+ sessionId,
+ remoteId,
+ content,
+ contentType,
+ sequence
+ })
before(data)
this.msgIdRefillCallback[sequence] = after
this.sendAgent(data)
diff --git a/src/models/message.js b/src/models/message.js
index 3a15c71..352e60d 100644
--- a/src/models/message.js
+++ b/src/models/message.js
@@ -64,6 +64,7 @@ class Message {
fromId,
msgType,
content,
+ contentType,
msgTime,
sendTime = undefined,
msgId = undefined,
@@ -78,6 +79,7 @@ class Message {
this.fromId = fromId
this.msgType = msgType
this.content = content
+ this.contentType = contentType
this.status = status
this.msgTime = msgTime
this.sendTime = sendTime
diff --git a/src/proto/msg.js b/src/proto/msg.js
index a1261ff..a23b82e 100644
--- a/src/proto/msg.js
+++ b/src/proto/msg.js
@@ -819,6 +819,7 @@ export const Body = ($root.Body = (() => {
* @property {string|null} [groupId] Body groupId
* @property {number|Long|null} [msgId] Body msgId
* @property {string|null} [content] Body content
+ * @property {number|null} [contentType] Body contentType
* @property {string|null} [seq] Body seq
* @property {string|null} [sessionId] Body sessionId
*/
@@ -836,8 +837,9 @@ export const Body = ($root.Body = (() => {
* | 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
* | 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
* | 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
- * | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
- * | 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
+ * | 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
+ * | 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
+ * |10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
* +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
@@ -848,8 +850,9 @@ export const Body = ($root.Body = (() => {
* | 5 | groupId | - | - | - | M | M | M | o | - |
* | 6 | msgId | - | - | - | M | - | M | M | M |
* | 7 | content | M | M | M | M | M | M | M | - |
- * | 8 | seq | - | - | - | - | M | M | - | - |
- * | 9 | sessionId | - | - | - | M | M | M | M | M |
+ * | 8 | contentType | - | - | - | - | - | - | - | - |
+ * | 9 | seq | - | - | - | - | M | M | - | - |
+ * |10 | sessionId | - | - | - | M | M | M | M | M |
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
* @implements IBody
* @constructor
@@ -917,6 +920,14 @@ export const Body = ($root.Body = (() => {
*/
Body.prototype.content = null
+ /**
+ * Body contentType.
+ * @member {number|null|undefined} contentType
+ * @memberof Body
+ * @instance
+ */
+ Body.prototype.contentType = null
+
/**
* Body seq.
* @member {string|null|undefined} seq
@@ -978,6 +989,12 @@ export const Body = ($root.Body = (() => {
set: $util.oneOfSetter($oneOfFields)
})
+ // Virtual OneOf for proto3 optional field
+ Object.defineProperty(Body.prototype, '_contentType', {
+ get: $util.oneOfGetter(($oneOfFields = ['contentType'])),
+ set: $util.oneOfSetter($oneOfFields)
+ })
+
// Virtual OneOf for proto3 optional field
Object.defineProperty(Body.prototype, '_seq', {
get: $util.oneOfGetter(($oneOfFields = ['seq'])),
@@ -1027,10 +1044,12 @@ export const Body = ($root.Body = (() => {
writer.uint32(/* id 6, wireType 0 =*/ 48).int64(message.msgId)
if (message.content != null && Object.hasOwnProperty.call(message, 'content'))
writer.uint32(/* id 7, wireType 2 =*/ 58).string(message.content)
+ if (message.contentType != null && Object.hasOwnProperty.call(message, 'contentType'))
+ writer.uint32(/* id 8, wireType 0 =*/ 64).int32(message.contentType)
if (message.seq != null && Object.hasOwnProperty.call(message, 'seq'))
- writer.uint32(/* id 8, wireType 2 =*/ 66).string(message.seq)
+ writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.seq)
if (message.sessionId != null && Object.hasOwnProperty.call(message, 'sessionId'))
- writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.sessionId)
+ writer.uint32(/* id 10, wireType 2 =*/ 82).string(message.sessionId)
return writer
}
@@ -1094,10 +1113,14 @@ export const Body = ($root.Body = (() => {
break
}
case 8: {
- message.seq = reader.string()
+ message.contentType = reader.int32()
break
}
case 9: {
+ message.seq = reader.string()
+ break
+ }
+ case 10: {
message.sessionId = reader.string()
break
}
@@ -1171,6 +1194,10 @@ export const Body = ($root.Body = (() => {
properties._content = 1
if (!$util.isString(message.content)) return 'content: string expected'
}
+ if (message.contentType != null && message.hasOwnProperty('contentType')) {
+ properties._contentType = 1
+ if (!$util.isInteger(message.contentType)) return 'contentType: integer expected'
+ }
if (message.seq != null && message.hasOwnProperty('seq')) {
properties._seq = 1
if (!$util.isString(message.seq)) return 'seq: string expected'
@@ -1208,6 +1235,7 @@ export const Body = ($root.Body = (() => {
object.msgId.high >>> 0
).toNumber()
if (object.content != null) message.content = String(object.content)
+ if (object.contentType != null) message.contentType = object.contentType | 0
if (object.seq != null) message.seq = String(object.seq)
if (object.sessionId != null) message.sessionId = String(object.sessionId)
return message
@@ -1261,6 +1289,10 @@ export const Body = ($root.Body = (() => {
object.content = message.content
if (options.oneofs) object._content = 'content'
}
+ if (message.contentType != null && message.hasOwnProperty('contentType')) {
+ object.contentType = message.contentType
+ if (options.oneofs) object._contentType = 'contentType'
+ }
if (message.seq != null && message.hasOwnProperty('seq')) {
object.seq = message.seq
if (options.oneofs) object._seq = 'seq'
diff --git a/src/proto/msg.proto b/src/proto/msg.proto
index 151d8a7..e3e058b 100644
--- a/src/proto/msg.proto
+++ b/src/proto/msg.proto
@@ -65,8 +65,9 @@ message Header {
| 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
| 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
| 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
-| 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
-| 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
+| 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
+| 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
+|10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
+---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
@@ -77,8 +78,9 @@ message Header {
| 5 | groupId | - | - | - | M | M | M | o | - |
| 6 | msgId | - | - | - | M | - | M | M | M |
| 7 | content | M | M | M | M | M | M | M | - |
-| 8 | seq | - | - | - | - | M | M | - | - |
-| 9 | sessionId | - | - | - | M | M | M | M | M |
+| 8 | contentType | - | - | - | - | - | - | - | - |
+| 9 | seq | - | - | - | - | M | M | - | - |
+|10 | sessionId | - | - | - | M | M | M | M | M |
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
*/
message Body {
@@ -89,8 +91,9 @@ message Body {
optional string groupId = 5;
optional int64 msgId = 6; //服务端生成的消息ID,会话内单调递增,可用于消息排序
optional string content = 7;
- optional string seq = 8; //客户端生成的序列号ID,会话内唯一,可用于消息去重
- optional string sessionId = 9; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
+ optional int32 contentType = 8;
+ optional string seq = 9; //客户端生成的序列号ID,会话内唯一,可用于消息去重
+ optional string sessionId = 10; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
}
message Extension {
diff --git a/src/stores/audio.js b/src/stores/audio.js
index bfb0b43..3ff0f14 100644
--- a/src/stores/audio.js
+++ b/src/stores/audio.js
@@ -18,20 +18,18 @@ export const useAudioStore = defineStore('anylink-audio', () => {
audio.value[obj.objectId] = obj
}
- const preloadAudio = async (msgRecords) => {
+ const preloadAudioFromMsgList = async (msgRecords) => {
const audioIds = new Set()
msgRecords.forEach((item) => {
- const content = item.content
- const contentJson = jsonParseSafe(content)
- if (
- (contentJson && contentJson['type'] === msgContentType.RECORDING) ||
- (contentJson && contentJson['type'] === msgContentType.AUDIO)
- ) {
- const objectId = contentJson['value']
- if (!audio.value[objectId]) {
- audioIds.add(objectId)
+ const aar = jsonParseSafe(item.content)
+ aar.forEach((item) => {
+ if (item.type === msgContentType.AUDIO || item.type === msgContentType.RECORDING) {
+ const objectId = item.value
+ if (!audio.value[objectId]) {
+ audioIds.add(objectId)
+ }
}
- }
+ })
})
if (audioIds.size > 0) {
@@ -55,7 +53,7 @@ export const useAudioStore = defineStore('anylink-audio', () => {
return {
audio,
setAudio,
- preloadAudio,
+ preloadAudioFromMsgList,
clear
}
})
diff --git a/src/stores/document.js b/src/stores/document.js
index d0e7e76..e4a6c83 100644
--- a/src/stores/document.js
+++ b/src/stores/document.js
@@ -18,17 +18,18 @@ export const useDocumentStore = defineStore('anylink-document', () => {
document.value[obj.objectId] = obj
}
- const preloadDocument = async (msgRecords) => {
+ const preloadDocumentFromMsgList = async (msgRecords) => {
const documentIds = new Set()
msgRecords.forEach((item) => {
- const content = item.content
- const contentJson = jsonParseSafe(content)
- if (contentJson && contentJson['type'] === msgContentType.DOCUMENT) {
- const objectId = contentJson['value']
- if (!document.value[objectId]) {
- documentIds.add(objectId)
+ const aar = jsonParseSafe(item.content)
+ aar.forEach((item) => {
+ if (item.type === msgContentType.DOCUMENT) {
+ const objectId = item.value
+ if (!document.value[objectId]) {
+ documentIds.add(objectId)
+ }
}
- }
+ })
})
if (documentIds.size > 0) {
@@ -52,7 +53,7 @@ export const useDocumentStore = defineStore('anylink-document', () => {
return {
document,
setDocument,
- preloadDocument,
+ preloadDocumentFromMsgList,
clear
}
})
diff --git a/src/stores/image.js b/src/stores/image.js
index a0a111e..ea325d7 100644
--- a/src/stores/image.js
+++ b/src/stores/image.js
@@ -4,8 +4,6 @@ import { jsonParseSafe } from '@/js/utils/common'
import { defineStore } from 'pinia'
import { ref } from 'vue'
-const pattern = /\{[a-f0-9]+\}/g
-
// image的缓存数据,不持久化存储
export const useImageStore = defineStore('anylink-image', () => {
/**
@@ -43,19 +41,20 @@ export const useImageStore = defineStore('anylink-image', () => {
}
}
- const loadImageInfoFromContent = async (content) => {
+ const preloadImageFromMsg = async (content) => {
+ if (!content) return
+
const imageIds = new Set()
- const matches = content.match(pattern)
- if (matches && matches.length > 0) {
- matches.forEach((item) => {
- let startIndex = item.indexOf('{')
- let endIndex = item.indexOf('}')
- const objectId = item.slice(startIndex + 1, endIndex)
+ const aar = jsonParseSafe(content)
+ aar.forEach((item) => {
+ if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
+ const objectId = item.value
if (!image.value[objectId]) {
imageIds.add(objectId)
}
- })
- }
+ }
+ })
+
if (imageIds.size > 0) {
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
res.data.data.forEach((item) => {
@@ -64,29 +63,18 @@ export const useImageStore = defineStore('anylink-image', () => {
}
}
- const preloadImage = async (msgRecords) => {
+ const preloadImageFromMsgList = async (msgRecords) => {
const imageIds = new Set()
msgRecords.forEach((item) => {
- const content = item.content
- const contentJson = jsonParseSafe(content)
- if (contentJson && contentJson['type'] === msgContentType.IMAGE) {
- const objectId = contentJson['value']
- if (!image.value[objectId]) {
- imageIds.add(objectId)
+ const aar = jsonParseSafe(item.content)
+ aar.forEach((item) => {
+ if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
+ const objectId = item.value
+ if (!image.value[objectId]) {
+ imageIds.add(objectId)
+ }
}
- } else {
- const matches = content.match(pattern)
- if (matches && matches.length > 0) {
- matches.forEach((item) => {
- let startIndex = item.indexOf('{')
- let endIndex = item.indexOf('}')
- const objectId = item.slice(startIndex + 1, endIndex)
- if (!image.value[objectId]) {
- imageIds.add(objectId)
- }
- })
- }
- }
+ })
})
if (imageIds.size > 0) {
@@ -117,8 +105,8 @@ export const useImageStore = defineStore('anylink-image', () => {
setImage,
setImageInSession,
clearImageInSession,
- loadImageInfoFromContent,
- preloadImage,
+ preloadImageFromMsg,
+ preloadImageFromMsgList,
clear
}
})
diff --git a/src/stores/message.js b/src/stores/message.js
index 6349c51..ab0f53b 100644
--- a/src/stores/message.js
+++ b/src/stores/message.js
@@ -134,10 +134,10 @@ export const useMessageStore = defineStore('anylink-message', () => {
* @param {*} msgRecords
*/
const preloadResource = async (msgRecords) => {
- await useImageStore().preloadImage(msgRecords)
- await useAudioStore().preloadAudio(msgRecords)
- await useVideoStore().preloadVideo(msgRecords)
- await useDocumentStore().preloadDocument(msgRecords)
+ await useImageStore().preloadImageFromMsgList(msgRecords)
+ await useAudioStore().preloadAudioFromMsgList(msgRecords)
+ await useVideoStore().preloadVideoFromMsgList(msgRecords)
+ await useDocumentStore().preloadDocumentFromMsgList(msgRecords)
}
/**
diff --git a/src/stores/video.js b/src/stores/video.js
index d932262..9d745ef 100644
--- a/src/stores/video.js
+++ b/src/stores/video.js
@@ -18,17 +18,18 @@ export const useVideoStore = defineStore('anylink-video', () => {
video.value[obj.objectId] = obj
}
- const preloadVideo = async (msgRecords) => {
+ const preloadVideoFromMsgList = async (msgRecords) => {
const videoIds = new Set()
msgRecords.forEach((item) => {
- const content = item.content
- const contentJson = jsonParseSafe(content)
- if (contentJson && contentJson['type'] === msgContentType.VIDEO) {
- const objectId = contentJson['value']
- if (!video.value[objectId]) {
- videoIds.add(objectId)
+ const aar = jsonParseSafe(item.content)
+ aar.forEach((item) => {
+ if (item.type === msgContentType.VIDEO) {
+ const objectId = item.value
+ if (!video.value[objectId]) {
+ videoIds.add(objectId)
+ }
}
- }
+ })
})
if (videoIds.size > 0) {
@@ -52,7 +53,7 @@ export const useVideoStore = defineStore('anylink-video', () => {
return {
video,
setVideo,
- preloadVideo,
+ preloadVideoFromMsgList,
clear
}
})
diff --git a/src/views/message/MessageLayout.vue b/src/views/message/MessageLayout.vue
index 4c9f400..b56f3dc 100644
--- a/src/views/message/MessageLayout.vue
+++ b/src/views/message/MessageLayout.vue
@@ -43,7 +43,7 @@ import { onReceiveChatMsg, onReceiveGroupChatMsg, onReceiveGroupSystemMsg } from
import { userQueryService } from '@/api/user'
import { ElLoading, ElMessage } from 'element-plus'
import { el_loading_options } from '@/const/commonConst'
-import { combineId, sessionIdConvert } from '@/js/utils/common'
+import { combineId, jsonParseSafe, sessionIdConvert } from '@/js/utils/common'
import MenuSession from '@/views/message/components/MenuSession.vue'
import router from '@/router'
import { BEGIN_MSG_ID, msgContentType, msgSendStatus } from '@/const/msgConst'
@@ -120,29 +120,45 @@ const unreadAtRecords = computed(() => {
})
const handleShowHighlight = (msgId) => {
- const readElement = document.querySelector(
+ let targetKey = msgId
+ let element = document.querySelector(
`#message-item-${sessionIdConvert(selectedSessionId.value)}-${msgId}`
- )
- if (!readElement) {
- ElMessage.success('请加载更多消息后查找')
- } else {
- const msgListRect = msgListDiv.value.getBoundingClientRect()
- const rect = readElement.getBoundingClientRect()
- // 判断 readElement 是否在 msgListDiv 的视口内
- const isInViewport = rect.top >= msgListRect.top && rect.bottom <= msgListRect.bottom
- if (!isInViewport) {
- nextTick(() => {
- msgListDiv.value.scrollTo({
- top: msgListDiv.value.scrollTop - (msgListRect.top - rect.top),
- behavior: 'smooth'
- })
- })
+ ) // 先拿msgId定位到元素,如果不行再用msgKey
+ if (!element) {
+ // 用msgId逆向找msgKey
+ for (const msgKey of msgKeysShow.value) {
+ const msg = messageData.getMsg(selectedSessionId.value, msgKey)
+ if (msg.msgId == msgId) {
+ targetKey = msgKey
+ element = document.querySelector(
+ `#message-item-${sessionIdConvert(selectedSessionId.value)}-${msgKey}`
+ )
+ break
+ }
+ }
+
+ if (!element) {
+ ElMessage.success('请加载更多消息后查找')
+ return
}
- highlightedMsgIds.value.add(msgId + '')
- setTimeout(() => {
- highlightedMsgIds.value.delete(msgId + '')
- }, 2000)
}
+
+ const msgListRect = msgListDiv.value.getBoundingClientRect()
+ const rect = element.getBoundingClientRect()
+ // 判断 element 是否在 msgListDiv 的视口内
+ const isInViewport = rect.top >= msgListRect.top && rect.bottom <= msgListRect.bottom
+ if (!isInViewport) {
+ nextTick(() => {
+ msgListDiv.value.scrollTo({
+ top: msgListDiv.value.scrollTop - (msgListRect.top - rect.top),
+ behavior: 'smooth'
+ })
+ })
+ }
+ highlightedMsgIds.value.add(targetKey + '')
+ setTimeout(() => {
+ highlightedMsgIds.value.delete(targetKey + '')
+ }, 2000)
}
const handleReadAt = () => {
@@ -563,7 +579,7 @@ const sendRead = () => {
selectedSession.value.sessionType === MsgType.CHAT
? MsgType.CHAT_READ
: MsgType.GROUP_CHAT_READ
- wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', '', () => {})
+ wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', 0, '', () => {})
// 更新本地缓存的已读位置
messageData.updateSession({
sessionId: selectedSessionId.value,
@@ -577,7 +593,7 @@ const sendRead = () => {
/**
* 处理发送转发的消息
*/
-const handleSendForwardMsg = async ({ session, content }) => {
+const handleSendForwardMsg = async ({ session, content, contentType }) => {
if (session.sessionType === MsgType.GROUP_CHAT && session.leave) {
ElMessage.warning('您已离开该群或群已被解散')
return
@@ -609,12 +625,13 @@ const handleSendForwardMsg = async ({ session, content }) => {
const seq = uuidv4()
const msg = {
msgId: seq,
- seq: seq,
+ seq,
sessionId: session.sessionId,
fromId: myAccount.value,
remoteId: session.remoteId,
msgType: session.sessionType,
- content: content,
+ content,
+ contentType,
status: msgSendStatus.PENDING,
msgTime: new Date(),
sendTime: new Date()
@@ -657,13 +674,22 @@ const handleSendForwardMsg = async ({ session, content }) => {
const after = (msgId) => {
messageData.updateMsg(msg.sessionId, msg.msgId, { msgId, status: msgSendStatus.OK })
}
- wsConnect.sendMsg(msg.sessionId, msg.remoteId, msg.msgType, msg.content, msg.seq, before, after)
+ wsConnect.sendMsg(
+ msg.sessionId,
+ msg.remoteId,
+ msg.msgType,
+ msg.content,
+ msg.contentType,
+ msg.seq,
+ before,
+ after
+ )
}
/**
* 发送时先添加本地消息,可以立即渲染
*/
-const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
+const handleLocalMsg = ({ content, contentType, fn }) => {
const seq = uuidv4()
const msg = {
msgId: seq,
@@ -671,10 +697,8 @@ const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
sessionId: selectedSessionId.value,
fromId: myAccount.value,
msgType: selectedSession.value.sessionType,
- content:
- contentType === msgContentType.MIX
- ? content
- : JSON.stringify({ type: contentType, value: objectId }),
+ content,
+ contentType,
status: msgSendStatus.PENDING,
msgTime: new Date(),
sendTime: new Date()
@@ -693,7 +717,7 @@ const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
fn(msg)
}
-const handleSendMessage = ({ msg, at }) => {
+const handleSendMessage = ({ msg, content, at }) => {
if (isNotInGroup.value) {
ElMessage.warning('您已离开该群或群已被解散')
return
@@ -754,7 +778,8 @@ const handleSendMessage = ({ msg, at }) => {
msg.sessionId,
showId.value,
selectedSession.value.sessionType,
- msg.content,
+ content,
+ msg.contentType,
msg.seq,
before,
after
@@ -774,10 +799,9 @@ const handleResendMessage = (msg) => {
})
const toSendAtList = []
- msg.content.split(/(<.*?>)/).forEach((item) => {
- if (item && item.startsWith('<') && item.endsWith('>')) {
- const index = item.indexOf('-')
- const account = item.slice(1, index) // 第一个字符是<,所以起点从1开始
+ jsonParseSafe(msg.content).forEach((item) => {
+ if (item.type === msgContentType.AT) {
+ const account = item.value.account
if (account == 0) {
toSendAtList.push(account)
} else {
@@ -787,6 +811,7 @@ const handleResendMessage = (msg) => {
}
}
})
+
handleSendMessage({ msg, at: toSendAtList })
}
@@ -800,7 +825,7 @@ const handleSendAt = (at, sessionId, referMsgId) => {
contentObj.isAtAll = true
} else {
contentObj.isAtAll = false
- contentObj.atList = at.filter((item) => groupMembers.value[item]) // 过滤脏数据
+ contentObj.atList = at.filter((item) => groupMembers.value[item]) // 过滤不是当前合法群成员的数据
}
if (contentObj.isAtAll || (!contentObj.isAtAll && contentObj.atList.length > 0)) {
@@ -809,6 +834,7 @@ const handleSendAt = (at, sessionId, referMsgId) => {
showId.value,
MsgType.AT,
JSON.stringify(contentObj),
+ 0,
null,
() => {},
() => {}
@@ -1386,7 +1412,8 @@ const handleConfirmForwardMsg = async (sessions) => {
const msg = messageData.getMsg(selectedSessionId.value, msgKey)
await handleSendForwardMsg({
session: item,
- content: msg.content
+ content: msg.content,
+ contentType: msg.contentType
})
}
} else if (showForwardMsgDialogTitle.value === '合并转发') {
@@ -1411,13 +1438,16 @@ const handleConfirmForwardMsg = async (sessions) => {
})
await handleSendForwardMsg({
session: item,
- content: JSON.stringify({
- type: msgContentType.FORWARD_TOGETHER,
- value: {
- sessionId: selectedSessionId.value,
- data: [...msgs]
+ content: JSON.stringify([
+ {
+ type: msgContentType.FORWARD_TOGETHER,
+ value: {
+ sessionId: selectedSessionId.value,
+ data: [...msgs]
+ }
}
- })
+ ]),
+ contentType: msgContentType.FORWARD_TOGETHER
})
}
}
@@ -1513,7 +1543,7 @@ const onConfirmSelect = async (selected) => {
const inputEditorRef = ref()
const onSendEmoji = (key) => {
- inputEditorRef.value.addEmoji(key)
+ inputEditorRef.value?.addEmoji(key)
}
const inputRecorderRef = ref(null)
diff --git a/src/views/message/components/DialogForMsgList.vue b/src/views/message/components/DialogForMsgList.vue
index 12d4b2e..2e52037 100644
--- a/src/views/message/components/DialogForMsgList.vue
+++ b/src/views/message/components/DialogForMsgList.vue
@@ -24,8 +24,8 @@ import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
import { emojis } from '@/js/utils/emojis'
-import { MsgType } from '@/proto/msg'
import { msgChatQueryMessagesService } from '@/api/message'
+import { showSimplifyMsgContent } from '@/js/utils/message'
const props = defineProps(['isShow', 'title', 'sessionId', 'msgs', 'tier'])
const emit = defineEmits(['update:isShow', 'showUserCard', 'close'])
@@ -38,13 +38,14 @@ const audioData = useAudioStore()
const videoData = useVideoStore()
const documentData = useDocumentStore()
-const msgsFromServer = ref({})
+const forwardMsgs = ref({})
+const quoteMsg = ref({})
onMounted(async () => {
const loadingInstance = ElLoading.service(el_loading_options)
try {
await messageData.preloadResource(props.msgs)
- await loadForwardTogetherMsgs()
+ await loadRelatedMsg()
} finally {
loadingInstance.close()
}
@@ -60,60 +61,58 @@ watch(
}
)
-const loadForwardTogetherMsgs = async () => {
+const loadRelatedMsg = async () => {
for (const msg of props.msgs) {
const content = msg.content
- const contentJson = jsonParseSafe(content)
- if (!contentJson) {
+ const arr = jsonParseSafe(content)
+ if (!arr) {
continue
}
- const type = contentJson['type']
- const value = contentJson['value']
- if (!type || !value) {
- continue
- } else {
- if (type === msgContentType.FORWARD_TOGETHER) {
- let res
- try {
- const msgIds = value.data
- .map((item) => {
- return item.msgId
- })
- .join(',')
- res = await msgChatQueryMessagesService({
- sessionId: value.sessionId,
- msgIds
+ for (const item of arr) {
+ if (item.type === msgContentType.QUOTE) {
+ // 先从本地消息缓存中获取
+ const msgFromStore = messageData.getMsg(msg.sessionId, item.value.msgId)
+ if (!msgFromStore.msgId) {
+ // 如果本地消息缓存中没有,再去服务器查询
+ const res = await msgChatQueryMessagesService({
+ sessionId: msg.sessionId,
+ msgIds: item.value.msgId
})
- } catch (error) {
- console.error(error)
- continue
- }
- const msgs = res.data.data
- if (!res.data.data || res.data.data.length == 0) {
- continue
- }
-
- // value.data(取里面的nickName) 和 msgs合一
- const newMsgs = {}
- msgs.forEach((item) => {
- newMsgs[item.msgId] = item
- })
- value.data.forEach((item) => {
- if (item.msgId in newMsgs) {
- newMsgs[item.msgId] = {
- ...newMsgs[item.msgId],
- ...item
- }
+ if (res.data.data && res.data.data.length > 0) {
+ quoteMsg.value[msg.msgId] = res.data.data[0]
}
- })
+ } else {
+ quoteMsg.value[msg.msgId] = msgFromStore
+ }
+ } else if (item.type === msgContentType.FORWARD_TOGETHER) {
+ if (!forwardMsgs.value[msg.msgId]) {
+ forwardMsgs.value[msg.msgId] = []
+ }
- msgsFromServer.value[msg.msgId] = Object.values(newMsgs).sort((a, b) => {
- const timeA = new Date(a.sendTime || a.msgTime).getTime()
- const timeB = new Date(b.sendTime || b.msgTime).getTime()
- return timeA - timeB
- })
+ const forwatdMsgIds = item.value.data.map((item) => item.msgId)
+ const toQueryMsgIds = []
+ for (const msgId of forwatdMsgIds) {
+ // 先从本地消息缓存中获取
+ const msgFromStore = messageData.getMsg(item.value.sessionId, msgId)
+ if (!msgFromStore.msgId) {
+ // 如果本地消息缓存中没有,再去服务器查询
+ toQueryMsgIds.push(msgId)
+ } else {
+ forwardMsgs.value[msg.msgId].push(msgFromStore)
+ }
+ }
+
+ if (toQueryMsgIds.length > 0) {
+ const res = await msgChatQueryMessagesService({
+ sessionId: item.value.sessionId,
+ msgIds: toQueryMsgIds.join(',')
+ })
+ res.data.data.forEach((item) => {
+ forwardMsgs.value[msg.msgId].push(item)
+ })
+ }
}
}
}
@@ -130,98 +129,62 @@ const isMyAccount = (account) => {
const renderContent = ({ msg }) => {
const content = msg.content
const msgId = msg.msgId
- const msgType = msg.msgType
- const contentJson = jsonParseSafe(content)
- if (!contentJson) {
- return renderMix(content, msgType)
+ const arr = jsonParseSafe(content)
+ // 不允许非结构化的content
+ if (!arr) {
+ return
}
- const type = contentJson['type']
- const value = contentJson['value']
- if (!type || !value) {
- return renderMix(content, msgType)
- }
-
- switch (type) {
- case msgContentType.MIX:
- return renderMix(value, msgType)
- case msgContentType.TEXT:
- return renderText(value)
- case msgContentType.RECORDING:
- return renderRecording(value)
- case msgContentType.AUDIO:
- return renderAudio(value)
- case msgContentType.IMAGE:
- return renderImage(value)
- case msgContentType.EMOJI:
- return renderEmoji(value)
- case msgContentType.VIDEO:
- return renderVideo(value, msgId)
- case msgContentType.DOCUMENT:
- return renderDocument(value)
- case msgContentType.FORWARD_TOGETHER:
- return renderForwardTogether(msgId)
- default:
- return {content}
- }
-}
-
-const renderMix = (content, msgType) => {
- if (!content) return h('div', [])
- let contentArray = []
-
- // 1. 先匹配quote引用内容
- content.split(/(「\{.*?\}」)/).forEach((item) => {
- if (item.startsWith('「{') && item.endsWith('}」')) {
- // quote引用内容直接添加如数组
- contentArray.push(item)
- } else {
- //2. 匹配内容中的图片
- item.split(/(\{\d+\})/).forEach((item) => {
- //3. 匹配内容中的表情
- item.split(/(\[.*?\])/).forEach((item) => {
- item.split(/(<.*?>)/).forEach((item) => {
- if (item) {
- contentArray.push(item)
- }
- })
- })
- })
+ return arr.map((item) => {
+ if (!item.type || !item.value) {
+ return
}
- })
- return contentArray.map((item) => {
- if (item.startsWith('{') && item.endsWith('}')) {
- return renderImage(item.slice(1, -1), true)
- } else if (item.startsWith('[') && item.endsWith(']')) {
- return renderEmoji(item.slice(1, -1))
- } else if (item.startsWith('<') && item.endsWith('>')) {
- return renderAt(item.slice(1, -1), msgType)
- } else if (item.startsWith('「{') && item.endsWith('}」')) {
- return renderQuote(item.slice(1, -1))
- } else {
- return {item.trim()}
+ switch (item.type) {
+ case msgContentType.TEXT:
+ return renderText(item.value)
+ case msgContentType.EMOJI:
+ return renderEmoji(item.value)
+ case msgContentType.SCREENSHOT:
+ return renderImage(item.value, true)
+ case msgContentType.AT:
+ return renderAt(item.value)
+ case msgContentType.QUOTE:
+ return renderQuote(item.value, msgId)
+
+ case msgContentType.IMAGE:
+ return renderImage(item.value)
+ case msgContentType.RECORDING:
+ return renderRecording(item.value)
+ case msgContentType.AUDIO:
+ return renderAudio(item.value)
+ case msgContentType.VIDEO:
+ return renderVideo(item.value, msgId)
+ case msgContentType.DOCUMENT:
+ return renderDocument(item.value)
+ case msgContentType.FORWARD_TOGETHER:
+ return renderForwardTogether(item.value, msgId)
+ default:
+ return
}
})
}
-const renderText = (content) => {
- return {content}
+const renderText = (text) => {
+ return {text}
}
-const renderRecording = (content) => {
- const audioId = content
+const renderRecording = (audioId) => {
const url = audioData.audio[audioId]?.downloadUrl
const duration = audioData.audio[audioId]?.duration
if (url) {
return
+ return
} else {
- return {content}
+ return {emojiId}
}
}
-const renderImage = (content, isForMix = false) => {
- const imgId = content
- imageData.setImageInSession(props.sessionId, imageData.image[imgId])
+const renderImage = (imgId, isScreenShot = false) => {
if (imageData.image[imgId]) {
+ imageData.setImageInSession(props.sessionId, imageData.image[imgId])
return (