mirror of
https://gitee.com/lijingbo-2021/open-anylink-web.git
synced 2025-12-30 02:52:26 +00:00
实现消息撤回功能
This commit is contained in:
@@ -12,6 +12,10 @@ export const msgChatPullMsgService = (obj) => {
|
||||
return request.get('/chat/pullMsg', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatRevokeMsgService = (obj) => {
|
||||
return request.post('/chat/revokeMsg', obj)
|
||||
}
|
||||
|
||||
export const msgAtService = () => {
|
||||
return request.get('/chat/queryAt')
|
||||
}
|
||||
|
||||
@@ -41,3 +41,13 @@ export const msgFileUploadStatus = {
|
||||
UPLOAD_SUCCESS: 2, // 上传成功
|
||||
UPLOAD_FAILED: 3 // 上传失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息撤回时间限制 10分钟
|
||||
*/
|
||||
export const MSG_REVOKE_TIME_LIMIT = 10 * 60 * 1000
|
||||
|
||||
/**
|
||||
* 消息撤回后能重新编辑的时间限制 2分钟
|
||||
*/
|
||||
export const MSG_REEDIT_TIME_LIMIT = 2 * 60 * 1000
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './receiveGroupChatMsg'
|
||||
export * from './receiveGroupChatReadMsg'
|
||||
export * from './receiveGroupSystemMsg'
|
||||
export * from './receiveAtMsg'
|
||||
export * from './receiveRevokeMsg'
|
||||
|
||||
10
src/js/event/receiveRevokeMsg.js
Normal file
10
src/js/event/receiveRevokeMsg.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
export const onReceiveRevokeMsg = () => {
|
||||
return (msg) => {
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const revokeMsgId = msg.body.content
|
||||
messageData.revokeMsgRcord(sessionId, revokeMsgId)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
onReceiveGroupChatMsg,
|
||||
onReceiveGroupChatReadMsg,
|
||||
onReceiveGroupSystemMsg,
|
||||
onReceiveAtMsg
|
||||
onReceiveAtMsg,
|
||||
onReceiveRevokeMsg
|
||||
} from '@/js/event'
|
||||
|
||||
class WsConnect {
|
||||
@@ -121,6 +122,7 @@ class WsConnect {
|
||||
[MsgType.GROUP_CHAT]: onReceiveGroupChatMsg(),
|
||||
[MsgType.GROUP_CHAT_READ]: onReceiveGroupChatReadMsg(),
|
||||
[MsgType.AT]: onReceiveAtMsg(),
|
||||
[MsgType.REVOKE]: onReceiveRevokeMsg(),
|
||||
[MsgType.SYS_GROUP_CREATE]: onReceiveGroupSystemMsg(),
|
||||
[MsgType.SYS_GROUP_ADD_MEMBER]: onReceiveGroupSystemMsg(),
|
||||
[MsgType.SYS_GROUP_DEL_MEMBER]: onReceiveGroupSystemMsg(),
|
||||
|
||||
@@ -294,6 +294,7 @@ export const Msg = ($root.Msg = (() => {
|
||||
* @property {number} STATUS_RES=9 STATUS_RES value
|
||||
* @property {number} STATUS_SYNC=10 STATUS_SYNC value
|
||||
* @property {number} AT=11 AT value
|
||||
* @property {number} REVOKE=12 REVOKE value
|
||||
* @property {number} SYS_GROUP_CREATE=21 SYS_GROUP_CREATE value
|
||||
* @property {number} SYS_GROUP_ADD_MEMBER=22 SYS_GROUP_ADD_MEMBER value
|
||||
* @property {number} SYS_GROUP_DEL_MEMBER=23 SYS_GROUP_DEL_MEMBER value
|
||||
@@ -331,6 +332,7 @@ export const MsgType = ($root.MsgType = (() => {
|
||||
values[(valuesById[9] = 'STATUS_RES')] = 9
|
||||
values[(valuesById[10] = 'STATUS_SYNC')] = 10
|
||||
values[(valuesById[11] = 'AT')] = 11
|
||||
values[(valuesById[12] = 'REVOKE')] = 12
|
||||
values[(valuesById[21] = 'SYS_GROUP_CREATE')] = 21
|
||||
values[(valuesById[22] = 'SYS_GROUP_ADD_MEMBER')] = 22
|
||||
values[(valuesById[23] = 'SYS_GROUP_DEL_MEMBER')] = 23
|
||||
@@ -546,6 +548,7 @@ export const Header = ($root.Header = (() => {
|
||||
case 9:
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 21:
|
||||
case 22:
|
||||
case 23:
|
||||
@@ -642,6 +645,10 @@ export const Header = ($root.Header = (() => {
|
||||
case 11:
|
||||
message.msgType = 11
|
||||
break
|
||||
case 'REVOKE':
|
||||
case 12:
|
||||
message.msgType = 12
|
||||
break
|
||||
case 'SYS_GROUP_CREATE':
|
||||
case 21:
|
||||
message.msgType = 21
|
||||
@@ -825,18 +832,18 @@ export const Body = ($root.Body = (() => {
|
||||
* | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
* | 9 | 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)
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+
|
||||
* | 1 | fromId | M | M | M | - | M | M |
|
||||
* | 2 | fromClient | M | M | M | - | M | M |
|
||||
* | 3 | toId | - | - | - | - | - | M |
|
||||
* | 4 | toClient | - | - | - | - | - | M |
|
||||
* | 5 | groupId | - | - | - | M | M | M |
|
||||
* | 6 | msgId | - | - | - | M | - | M |
|
||||
* | 7 | content | M | M | M | M | M | M |
|
||||
* | 8 | seq | - | - | - | - | M | M |
|
||||
* | 9 | sessionId | - | - | - | M | M | M |
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+
|
||||
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+
|
||||
* | 1 | fromId | M | M | M | - | M | M | M |
|
||||
* | 2 | fromClient | M | M | M | - | M | M | - |
|
||||
* | 3 | toId | - | - | - | - | - | M | o |
|
||||
* | 4 | toClient | - | - | - | - | - | M | - |
|
||||
* | 5 | groupId | - | - | - | M | M | M | o |
|
||||
* | 6 | msgId | - | - | - | M | - | M | M |
|
||||
* | 7 | content | M | M | M | M | M | M | M |
|
||||
* | 8 | seq | - | - | - | - | M | M | - |
|
||||
* | 9 | sessionId | - | - | - | M | M | M | M |
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+
|
||||
* @implements IBody
|
||||
* @constructor
|
||||
* @param {IBody=} [properties] Properties to set
|
||||
|
||||
@@ -19,6 +19,7 @@ enum MsgType {
|
||||
STATUS_RES = 9; //连接状态响应
|
||||
STATUS_SYNC = 10; //端侧的连接状态同步给云端(比如在线,离开)
|
||||
AT = 11; //@消息
|
||||
REVOKE = 12; //撤回消息
|
||||
|
||||
SYS_GROUP_CREATE = 21; //系统消息之创建群组
|
||||
SYS_GROUP_ADD_MEMBER = 22; //系统消息之添加群组成员
|
||||
@@ -66,18 +67,18 @@ message Header {
|
||||
| 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
| 9 | 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)
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+
|
||||
| 1 | fromId | M | M | M | - | M | M |
|
||||
| 2 | fromClient | M | M | M | - | M | M |
|
||||
| 3 | toId | - | - | - | - | - | M |
|
||||
| 4 | toClient | - | - | - | - | - | M |
|
||||
| 5 | groupId | - | - | - | M | M | M |
|
||||
| 6 | msgId | - | - | - | M | - | M |
|
||||
| 7 | content | M | M | M | M | M | M |
|
||||
| 8 | seq | - | - | - | - | M | M |
|
||||
| 9 | sessionId | - | - | - | M | M | M |
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+
|
||||
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+
|
||||
| 1 | fromId | M | M | M | - | M | M | M |
|
||||
| 2 | fromClient | M | M | M | - | M | M | - |
|
||||
| 3 | toId | - | - | - | - | - | M | o |
|
||||
| 4 | toClient | - | - | - | - | - | M | - |
|
||||
| 5 | groupId | - | - | - | M | M | M | o |
|
||||
| 6 | msgId | - | - | - | M | - | M | M |
|
||||
| 7 | content | M | M | M | M | M | M | M |
|
||||
| 8 | seq | - | - | - | - | M | M | - |
|
||||
| 9 | sessionId | - | - | - | M | M | M | M |
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+
|
||||
*/
|
||||
message Body {
|
||||
optional string fromId = 1;
|
||||
|
||||
@@ -181,6 +181,12 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const revokeMsgRcord = (sessionId, msgKey) => {
|
||||
if (msgRecordsList.value[sessionId] && msgKey in msgRecordsList.value[sessionId]) {
|
||||
msgRecordsList.value[sessionId][msgKey].revoke = true
|
||||
}
|
||||
}
|
||||
|
||||
const getMsg = (sessionId, msgKey) => {
|
||||
if (!msgRecordsList.value[sessionId] || !msgRecordsList.value[sessionId][msgKey]) {
|
||||
return ref({})
|
||||
@@ -333,6 +339,7 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
||||
updateMsgKeySort,
|
||||
addMsgRecords,
|
||||
removeMsgRecord,
|
||||
revokeMsgRcord,
|
||||
getMsg,
|
||||
updateMsg,
|
||||
|
||||
|
||||
@@ -1296,6 +1296,7 @@ const onShowRecorder = () => {
|
||||
:lastMsgId="lastMsgId"
|
||||
:hasNoMoreMsg="hasNoMoreMsg"
|
||||
:isLoadMoreLoading="selectedSessionCache[selectedSessionId]?.isLoadMoreLoading"
|
||||
:inputEditorRef="inputEditorRef"
|
||||
@loadMore="onLoadMore"
|
||||
@showUserCard="onShowUserCard"
|
||||
@showGroupCard="onShowGroupCard"
|
||||
|
||||
@@ -27,7 +27,7 @@ import AgreeBeforeSend from '@/views/message/components/AgreeBeforeSend.vue'
|
||||
const Clipboard = Quill.import('modules/clipboard')
|
||||
class PlainClipboard extends Clipboard {
|
||||
onPaste(range, { text }) {
|
||||
renderContent(text)
|
||||
handlePaste(range, text)
|
||||
}
|
||||
}
|
||||
Quill.register(
|
||||
@@ -110,7 +110,7 @@ onMounted(async () => {
|
||||
// 给组件增加滚动条样式
|
||||
document.querySelector('.ql-editor').classList.add('my-scrollbar')
|
||||
await imageData.loadImageInfoFromContent(props.draft)
|
||||
renderContent(props.draft)
|
||||
renderContent(props.draft) // 渲染草稿
|
||||
quill.value.on('composition-start', () => {
|
||||
// 当用户使用拼音输入法开始输入汉字时,这个事件就会被触发
|
||||
quill.value.root.dataset.placeholder = ''
|
||||
@@ -122,7 +122,7 @@ onMounted(async () => {
|
||||
|
||||
// 监听文本变化检测@符号
|
||||
quill.value.on('text-change', (delta, oldDelta, source) => {
|
||||
if (session.value.sessionType === MsgType.GROUP_CHAT && source === 'user') {
|
||||
if (session.value.sessionType === MsgType.GROUP_CHAT && source === Quill.sources.USER) {
|
||||
const insertOps = delta.ops.filter((op) => op.insert && typeof op.insert === 'string')
|
||||
const insertContent = insertOps.map((item) => item.insert).join('')
|
||||
if (insertContent.length > 0) {
|
||||
@@ -393,16 +393,76 @@ watch(
|
||||
|
||||
fn(contentObj.contentFromLocal.join('').trim())
|
||||
|
||||
renderContent(messageData.sessionList[newSessionId].draft || '')
|
||||
renderContent(messageData.sessionList[newSessionId].draft || '') // 切换session时渲染新session的草稿
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
let pasteContent
|
||||
let pasteContentType
|
||||
let pasteFileName
|
||||
let pasteFileSize
|
||||
let pasteUrl
|
||||
// 实现消息复制的效果,步骤如下
|
||||
// 1. 拷贝原消息中的content内容
|
||||
// 2. 粘贴时自动调用renderContent渲染内容
|
||||
// 3. 渲染时保存复制内容
|
||||
// 4. 发送时使用保存的复制内容
|
||||
const pasteObj = {
|
||||
content: null,
|
||||
contentType: null,
|
||||
fileName: null,
|
||||
fileSize: null,
|
||||
url: null
|
||||
}
|
||||
|
||||
const clearPasteObj = () => {
|
||||
pasteObj.content = null
|
||||
pasteObj.contentType = null
|
||||
pasteObj.fileName = null
|
||||
pasteObj.fileSize = null
|
||||
pasteObj.url = null
|
||||
}
|
||||
|
||||
const handlePaste = (range, content) => {
|
||||
if (!content) {
|
||||
return
|
||||
}
|
||||
|
||||
const jsonContent = jsonParseSafe(content)
|
||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
||||
clearPasteObj()
|
||||
pasteObj.content = content
|
||||
pasteObj.contentType = jsonContent['type']
|
||||
const fileId = jsonContent['value']
|
||||
switch (pasteObj.contentType) {
|
||||
case msgContentType.IMAGE:
|
||||
pasteObj.fileName = imageData.image[fileId]?.fileName
|
||||
pasteObj.fileSize = imageData.image[fileId]?.size
|
||||
pasteObj.url = imageData.image[fileId]?.thumbUrl
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
pasteObj.fileName = audioData.audio[fileId]?.fileName
|
||||
pasteObj.fileSize = audioData.audio[fileId]?.size
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
pasteObj.fileName = videoData.video[fileId]?.fileName
|
||||
pasteObj.fileSize = videoData.video[fileId]?.size
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
pasteObj.fileName = documentData.document[fileId]?.fileName
|
||||
pasteObj.fileSize = documentData.document[fileId]?.size
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// 文件确实存在才发送
|
||||
if (pasteObj.fileName) {
|
||||
showAgreeDialog.value = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const delta = new Delta().retain(range.index).delete(range.length).insert(content)
|
||||
quill.value.updateContents(delta, Quill.sources.USER)
|
||||
quill.value.setSelection(delta.length() - range.length, Quill.sources.USER)
|
||||
}
|
||||
|
||||
/**
|
||||
* 把输入框的字符串内容渲染成富媒体内容
|
||||
@@ -413,82 +473,62 @@ const renderContent = (content) => {
|
||||
quill.value.setText('')
|
||||
return
|
||||
}
|
||||
pasteContent = content
|
||||
const jsonContent = jsonParseSafe(content)
|
||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
||||
pasteContentType = jsonContent['type']
|
||||
const fileId = jsonContent['value']
|
||||
switch (pasteContentType) {
|
||||
case msgContentType.IMAGE:
|
||||
pasteFileName = imageData.image[fileId]?.fileName
|
||||
pasteFileSize = imageData.image[fileId]?.size
|
||||
pasteUrl = imageData.image[fileId]?.thumbUrl
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
pasteFileName = audioData.audio[fileId]?.fileName
|
||||
pasteFileSize = audioData.audio[fileId]?.size
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
pasteFileName = videoData.video[fileId]?.fileName
|
||||
pasteFileSize = videoData.video[fileId]?.size
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
pasteFileName = documentData.document[fileId]?.fileName
|
||||
pasteFileSize = documentData.document[fileId]?.size
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
showAgreeDialog.value = true
|
||||
} else {
|
||||
let contentArray = []
|
||||
//匹配内容中的图片
|
||||
content.split(/(\{.*?\})/).forEach((item) => {
|
||||
//匹配内容中的表情
|
||||
item.split(/(\[.*?\])/).forEach((item) => {
|
||||
//匹配内容中的@
|
||||
item.split(/(<.*?>)/).forEach((item) => {
|
||||
if (item) {
|
||||
contentArray.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
let contentArray = []
|
||||
//匹配内容中的图片
|
||||
content.split(/(\{.*?\})/).forEach((item) => {
|
||||
//匹配内容中的表情
|
||||
item.split(/(\[.*?\])/).forEach((item) => {
|
||||
//匹配内容中的@
|
||||
item.split(/(<.*?>)/).forEach((item) => {
|
||||
if (item) {
|
||||
contentArray.push(item)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 创建一个新的 Delta 对象
|
||||
const delta = new Delta()
|
||||
contentArray.map((item) => {
|
||||
if (item.startsWith('{') && item.endsWith('}')) {
|
||||
const imageId = item.slice(1, -1)
|
||||
const imageUrl = imageData.image[imageId].originUrl
|
||||
// 创建一个新的 Delta 对象
|
||||
const delta = new Delta()
|
||||
contentArray.map((item) => {
|
||||
if (item.startsWith('{') && item.endsWith('}')) {
|
||||
const imageId = item.slice(1, -1)
|
||||
const imageUrl = imageData.image[imageId]?.originUrl
|
||||
if (imageUrl) {
|
||||
delta.insert({ image: imageUrl }, { alt: item })
|
||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
||||
const emojiUrl = emojis[item]
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
||||
const emojiUrl = emojis[item]
|
||||
if (emojiUrl) {
|
||||
delta.insert({ image: emojiUrl }, { alt: item })
|
||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
||||
const content = item.slice(1, -1)
|
||||
const index = content.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const account = content.slice(0, index)
|
||||
const nickName = content.slice(index + 1)
|
||||
if (nickName) {
|
||||
toSendAtList.value.push(account)
|
||||
delta.insert({ atMention: { account, nickName } })
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
||||
const content = item.slice(1, -1)
|
||||
const index = content.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const account = content.slice(0, index)
|
||||
const nickName = content.slice(index + 1)
|
||||
if (nickName) {
|
||||
toSendAtList.value.push(account)
|
||||
delta.insert({ atMention: { account, nickName } })
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
delta.insert(item)
|
||||
}
|
||||
})
|
||||
|
||||
quill.value.setText('') // 清空编辑器内容
|
||||
quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容
|
||||
quill.value.setSelection(quill.value.getLength(), 0, 'user') // 设置光标位置
|
||||
}
|
||||
quill.value.setText('') // 清空编辑器内容
|
||||
quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容
|
||||
quill.value.setSelection(quill.value.getLength(), 0, Quill.sources.USER) // 设置光标位置
|
||||
}
|
||||
|
||||
const handleEnter = async () => {
|
||||
@@ -502,8 +542,8 @@ const handleEnter = async () => {
|
||||
allUploadedSuccessFn: () => {}
|
||||
}
|
||||
|
||||
const contentObj = pasteContent
|
||||
? { contentFromLocal: [pasteContent], contentFromServer: [pasteContent] }
|
||||
const contentObj = pasteObj.content
|
||||
? { contentFromLocal: [pasteObj.content], contentFromServer: [pasteObj.content] }
|
||||
: await parseContent(callbacks)
|
||||
|
||||
const content = contentObj.contentFromLocal.join('').trim()
|
||||
@@ -561,9 +601,10 @@ const handleEnter = async () => {
|
||||
emit('sendMessage', { msg, atTargets })
|
||||
}
|
||||
|
||||
pasteContent = ''
|
||||
quill.value.setText('') // 编辑窗口置空
|
||||
clearPasteObj()
|
||||
toSendAtList.value = []
|
||||
quill.value.setText('') // 编辑窗口置空
|
||||
quill.value.setSelection(0, 0, Quill.sources.USER) // 设置光标位置
|
||||
}
|
||||
|
||||
const options = {
|
||||
@@ -600,7 +641,7 @@ const addEmoji = (key) => {
|
||||
delta.retain(index)
|
||||
delta.insert({ image: emojis[key] }, { alt: key })
|
||||
quill.value.updateContents(delta)
|
||||
quill.value.setSelection(index + 1, 0, 'user')
|
||||
quill.value.setSelection(index + 1, 0, Quill.sources.USER)
|
||||
}
|
||||
|
||||
const onSelectedAtTarget = ({ account, nickName }) => {
|
||||
@@ -613,18 +654,30 @@ const onSelectedAtTarget = ({ account, nickName }) => {
|
||||
if (range.index >= atIndex.value) {
|
||||
const delLen = range.index - atIndex.value + 1 // 删除用户输入的@符号及搜索关键字
|
||||
quill.value.deleteText(atIndex.value - 1, delLen)
|
||||
quill.value.insertEmbed(atIndex.value - 1, 'atMention', { account, nickName }, 'user') // 插入Blot(占据1个位置)
|
||||
quill.value.insertText(atIndex.value, ' ', 'user') // 插入空格
|
||||
quill.value.setSelection(atIndex.value + 1, 0, 'user') // 定位光标
|
||||
quill.value.insertEmbed(
|
||||
atIndex.value - 1,
|
||||
'atMention',
|
||||
{ account, nickName },
|
||||
Quill.sources.USER
|
||||
) // 插入Blot(占据1个位置)
|
||||
quill.value.insertText(atIndex.value, ' ', Quill.sources.USER) // 插入空格
|
||||
quill.value.setSelection(atIndex.value + 1, 0, Quill.sources.USER) // 定位光标
|
||||
} else {
|
||||
quill.value.insertEmbed(range.index, 'atMention', { account, nickName }, 'user') // 插入Blot(占据1个位置)
|
||||
quill.value.insertText(range.index + 1, ' ', 'user') // 插入空格
|
||||
quill.value.setSelection(range.index + 1 + 1, 0, 'user') // 定位光标
|
||||
quill.value.insertEmbed(range.index, 'atMention', { account, nickName }, Quill.sources.USER) // 插入Blot(占据1个位置)
|
||||
quill.value.insertText(range.index + 1, ' ', Quill.sources.USER) // 插入空格
|
||||
quill.value.setSelection(range.index + 1 + 1, 0, Quill.sources.USER) // 定位光标
|
||||
}
|
||||
}
|
||||
|
||||
const reeditFromRevoke = (content) => {
|
||||
quill.value.setText('') // 清空编辑器内容
|
||||
quill.value.setSelection(0, 0, Quill.sources.SILENT) // 设置光标位置
|
||||
renderContent(content)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
addEmoji
|
||||
addEmoji,
|
||||
reeditFromRevoke
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -647,10 +700,10 @@ defineExpose({
|
||||
<AgreeBeforeSend
|
||||
v-model:isShow="showAgreeDialog"
|
||||
:target="remoteName"
|
||||
:contentType="pasteContentType"
|
||||
:fileName="pasteFileName"
|
||||
:fileSize="pasteFileSize"
|
||||
:src="pasteUrl"
|
||||
:contentType="pasteObj.contentType"
|
||||
:fileName="pasteObj.fileName"
|
||||
:fileSize="pasteObj.fileSize"
|
||||
:src="pasteObj.url"
|
||||
@confirm="handleEnter"
|
||||
></AgreeBeforeSend>
|
||||
</div>
|
||||
|
||||
@@ -6,18 +6,25 @@ import DeletemsgIcon from '@/assets/svg/deletemsg.svg'
|
||||
import CopyIcon from '@/assets/svg/copy.svg'
|
||||
import MultiselectIcon from '@/assets/svg/multiselect.svg'
|
||||
import RevokeIcon from '@/assets/svg/revoke.svg'
|
||||
import { useMenuStore } from '@/stores'
|
||||
import { useUserStore, useMenuStore } from '@/stores'
|
||||
import { jsonParseSafe } from '@/js/utils/common'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { MSG_REVOKE_TIME_LIMIT, msgContentType } from '@/const/msgConst'
|
||||
|
||||
const props = defineProps(['msg'])
|
||||
const emit = defineEmits(['selectMenu'])
|
||||
|
||||
const userData = useUserStore()
|
||||
const menuData = useMenuStore()
|
||||
const openMenuTime = ref(null)
|
||||
|
||||
const menuName = computed(() => {
|
||||
return 'MenuMsgItem-' + props.msg.msgId
|
||||
})
|
||||
|
||||
const myAccount = computed(() => {
|
||||
return userData.user.account
|
||||
})
|
||||
|
||||
const contentType = computed(() => {
|
||||
const contentJson = jsonParseSafe(props.msg.content)
|
||||
if (!contentJson) {
|
||||
@@ -37,38 +44,51 @@ const menu = computed(() => {
|
||||
{
|
||||
label: 'forward',
|
||||
desc: '转发',
|
||||
icon: markRaw(ForwardIcon)
|
||||
icon: markRaw(ForwardIcon),
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
label: 'multiSelect',
|
||||
desc: '多选',
|
||||
icon: markRaw(MultiselectIcon)
|
||||
icon: markRaw(MultiselectIcon),
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
label: 'quote',
|
||||
desc: '引用',
|
||||
icon: markRaw(QuoteIcon)
|
||||
},
|
||||
{
|
||||
label: 'revoke',
|
||||
desc: '撤回',
|
||||
icon: markRaw(RevokeIcon)
|
||||
icon: markRaw(QuoteIcon),
|
||||
index: 3
|
||||
},
|
||||
{
|
||||
label: 'delete',
|
||||
desc: '删除',
|
||||
icon: markRaw(DeletemsgIcon)
|
||||
icon: markRaw(DeletemsgIcon),
|
||||
index: 5
|
||||
}
|
||||
]
|
||||
|
||||
if (contentType.value !== msgContentType.RECORDING) {
|
||||
o.unshift({
|
||||
o.push({
|
||||
label: 'copy',
|
||||
desc: '复制',
|
||||
icon: markRaw(CopyIcon)
|
||||
icon: markRaw(CopyIcon),
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
return o
|
||||
|
||||
if (
|
||||
myAccount.value === props.msg.fromId &&
|
||||
openMenuTime.value - new Date(props.msg.msgTime) < MSG_REVOKE_TIME_LIMIT
|
||||
) {
|
||||
o.push({
|
||||
label: 'revoke',
|
||||
desc: '撤回',
|
||||
icon: markRaw(RevokeIcon),
|
||||
index: 4
|
||||
})
|
||||
}
|
||||
|
||||
return o.sort((a, b) => a.index - b.index)
|
||||
})
|
||||
|
||||
const containerRef = ref()
|
||||
@@ -106,6 +126,7 @@ const handleShowMenu = (e) => {
|
||||
e.stopPropagation() // 阻止冒泡
|
||||
isShowMenu.value = true
|
||||
menuData.setActiveMenu(menuName.value)
|
||||
openMenuTime.value = new Date()
|
||||
nextTick(() => {
|
||||
//如果发现菜单超出window.innerWidth屏幕宽度,x要修正一下,往左边弹出菜单
|
||||
if (e.clientX + menuRef.value.clientWidth > window.innerWidth) {
|
||||
@@ -129,6 +150,7 @@ const handleEscEvent = (event) => {
|
||||
|
||||
const closeMenu = () => {
|
||||
isShowMenu.value = false
|
||||
openMenuTime.value = null
|
||||
}
|
||||
|
||||
const handleClick = (item) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, h, createApp, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, h, createApp, watch, nextTick } from 'vue'
|
||||
import { WarningFilled } from '@element-plus/icons-vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { messageSysShowTime, showTimeFormat, jsonParseSafe } from '@/js/utils/common'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { emojis } from '@/js/utils/emojis'
|
||||
import { msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||
import { MSG_REEDIT_TIME_LIMIT, msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||
import MsgBoxRecording from '@/views/message/components/MsgBoxRecording.vue'
|
||||
import MsgBoxImage from '@/views/message/components/MsgBoxImage.vue'
|
||||
import MsgBoxAudio from '@/views/message/components/MsgBoxAudio.vue'
|
||||
@@ -23,6 +23,7 @@ import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
|
||||
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
||||
import MenuMsgItem from '@/views/message/components/MenuMsgItem.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { msgChatRevokeMsgService } from '@/api/message'
|
||||
|
||||
const props = defineProps([
|
||||
'sessionId',
|
||||
@@ -34,7 +35,8 @@ const props = defineProps([
|
||||
'firstMsgId',
|
||||
'lastMsgId',
|
||||
'hasNoMoreMsg',
|
||||
'isLoadMoreLoading'
|
||||
'isLoadMoreLoading',
|
||||
'inputEditorRef'
|
||||
])
|
||||
const emit = defineEmits(['loadMore', 'showUserCard', 'showGroupCard', 'resendMsg', 'loadFinished'])
|
||||
|
||||
@@ -100,7 +102,7 @@ const renderComponent = (content) => {
|
||||
case msgContentType.DOCUMENT:
|
||||
return renderDocument(value)
|
||||
default:
|
||||
return h('div', [])
|
||||
return h('span', content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +627,32 @@ const sysShowTime = computed(() => {
|
||||
return messageSysShowTime(new Date(msg.value.msgTime))
|
||||
})
|
||||
|
||||
const isRevoke = computed(() => {
|
||||
return msg.value.revoke
|
||||
})
|
||||
|
||||
const isReedit = computed(() => {
|
||||
const contentJson = jsonParseSafe(msg.value.content)
|
||||
if (!contentJson) {
|
||||
return true
|
||||
}
|
||||
|
||||
const type = contentJson['type']
|
||||
if (
|
||||
type === msgContentType.IMAGE ||
|
||||
type === msgContentType.RECORDING ||
|
||||
type === msgContentType.AUDIO ||
|
||||
type === msgContentType.VIDEO ||
|
||||
type === msgContentType.DOCUMENT
|
||||
) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
const isReeditTimeOut = ref(true)
|
||||
|
||||
// 判断是否是连续的会话,与上个会话时间差小于5分钟
|
||||
const isContinuousSession = computed(() => {
|
||||
if (!props.extend.preMsgTime) {
|
||||
@@ -672,7 +700,6 @@ const onResendMsg = () => {
|
||||
}
|
||||
|
||||
const onSelectMenuMsgItem = async (label) => {
|
||||
console.log(label)
|
||||
switch (label) {
|
||||
case 'copy':
|
||||
try {
|
||||
@@ -682,11 +709,37 @@ const onSelectMenuMsgItem = async (label) => {
|
||||
ElMessage.error('复制出错')
|
||||
}
|
||||
break
|
||||
case 'revoke':
|
||||
msgChatRevokeMsgService({
|
||||
sessionId: props.sessionId,
|
||||
revokeMsgId: msg.value.msgId, // 服务器上删除用msg.value.msgId
|
||||
isGroupChat: isGroupChatMsgType.value,
|
||||
remoteId: messageData.sessionList[props.sessionId].remoteId
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
// 本地删除用props.msgKey,因为key有可能是发送消息时产生的本地UUID
|
||||
messageData.revokeMsgRcord(props.sessionId, props.msgKey)
|
||||
isReeditTimeOut.value = false
|
||||
setTimeout(() => {
|
||||
isReeditTimeOut.value = true
|
||||
}, MSG_REEDIT_TIME_LIMIT)
|
||||
ElMessage.success('消息已撤回')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleReedit = () => {
|
||||
props.inputEditorRef.reeditFromRevoke(msg.value.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 只要内容发生变化,重新渲染
|
||||
*/
|
||||
@@ -725,6 +778,22 @@ watch(
|
||||
v-html="systemMsgContent"
|
||||
@click="onClickSystemMsg"
|
||||
></div>
|
||||
<div v-else-if="!isSystemMsg && isRevoke" class="revoke-wrapper">
|
||||
<div v-if="isSelf">
|
||||
<span>你撤回了一条消息</span>
|
||||
<span
|
||||
v-if="isReedit && !isReeditTimeOut"
|
||||
style="margin-left: 2px; color: #409eff; cursor: pointer"
|
||||
@click="handleReedit"
|
||||
>
|
||||
重新编辑
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="isChatMsgType">对方撤回了一条消息</div>
|
||||
<div v-else>{{ `“${objectInfo.nickName}”撤回了一条消息` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="message-container-wrapper">
|
||||
<el-container class="el-container-right" v-if="isSelf">
|
||||
<el-main class="el-main-right">
|
||||
@@ -880,6 +949,16 @@ watch(
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.revoke-wrapper {
|
||||
padding: 2px 4px 2px 4px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.message-container-wrapper {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
|
||||
Reference in New Issue
Block a user