mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-22 05:59:58 +00:00
feat(dialog): 撤回文本消息后本地显示"重新编辑"占位
- 撤回自己的文本/MD 消息后,原位置显示仅本人可见的"你撤回了一条消息 重新编辑"占位(仿微信) - 占位纯本地存储(IndexedDB 持久化,Electron 多窗口 syncDispatch 同步),5 分钟过期自动清除 - 点"重新编辑"回填原内容到输入框(富文本还原 @提及/表情,先退出引用/编辑模式),占位随即移除 - 占位沿用原消息 id 排序定位,仅在已加载消息范围内合并显示,不干扰翻页与已读上报 - 同步更新 ai-kb recall 文档与前端语言文件
This commit is contained in:
parent
966c7b4ef0
commit
f7bc724e13
@ -2532,3 +2532,5 @@ AI任务分析
|
||||
我共享的
|
||||
共享给我的
|
||||
服务器返回错误(HTTP (*))
|
||||
你撤回了一条消息
|
||||
重新编辑
|
||||
|
||||
@ -12,6 +12,8 @@ aliases:
|
||||
- 收回消息
|
||||
- 误发了能撤回吗
|
||||
- 多久内可以撤回
|
||||
- 撤回后重新编辑
|
||||
- 撤回了还能改吗
|
||||
related_tools: []
|
||||
related_pages: [messenger, dialog_chat]
|
||||
prerequisites:
|
||||
@ -21,12 +23,15 @@ negative:
|
||||
- 默认 msg_rev_limit 为空表示不限制;若设置成 0 也表示不限制
|
||||
- 机器人发的消息可以无限期撤回(msg_rev_limit 校验对机器人放行)
|
||||
- 个人自聊会话(isSelfDialog)可以无限期撤回
|
||||
last_verified: v1.7.90
|
||||
- 「重新编辑」入口仅本人、仅撤回时所在的设备可见,且只保留 5 分钟;文件、语音等非文本消息撤回后没有该入口
|
||||
last_verified: v1.8.69
|
||||
---
|
||||
|
||||
# 撤回消息
|
||||
|
||||
撤回消息(withdraw)会软删除自己发送的消息(deletes 字段),群里所有人看到「XXX 撤回了一条消息」占位。撤回时限受系统配置 `msg_rev_limit` 控制(分钟)。
|
||||
撤回消息(withdraw)会软删除自己发送的消息(deletes 字段),消息从会话中对所有人消失。撤回时限受系统配置 `msg_rev_limit` 控制(分钟)。
|
||||
|
||||
撤回文本消息后,本人在当前设备的原消息位置会看到仅自己可见的「你撤回了一条消息 重新编辑」占位(本地保存,5 分钟内有效),点「重新编辑」可把原内容回填到输入框修改后重新发送。
|
||||
|
||||
## 入口
|
||||
|
||||
@ -52,6 +57,13 @@ last_verified: v1.7.90
|
||||
| 机器人发的消息 | 无限制 |
|
||||
| 自聊(私人云笔记会话) | 无限制 |
|
||||
|
||||
## 撤回后重新编辑
|
||||
|
||||
- 仅撤回**文本消息**(含 Markdown)时出现「你撤回了一条消息 **重新编辑**」占位
|
||||
- 占位只有自己可见,且只保存在执行撤回操作的这台设备本地(换设备、其他成员都看不到)
|
||||
- 撤回后 **5 分钟内**有效,过期自动消失
|
||||
- 点「重新编辑」:原内容回填到输入框(会覆盖输入框中已有内容),占位随即消失
|
||||
|
||||
## 与「编辑」的区别
|
||||
|
||||
- 撤回:消息消失,留占位
|
||||
|
||||
@ -39,6 +39,10 @@
|
||||
<div v-else-if="source.type === 'notice'" class="dialog-notice">
|
||||
{{source.msg.source === 'api' ? source.msg.notice : $L(source.msg.notice)}}
|
||||
</div>
|
||||
<div v-else-if="source.type === 'withdraw'" class="dialog-withdraw">
|
||||
{{$L('你撤回了一条消息')}}
|
||||
<em class="withdraw-re-edit" @click="onWithdrawReEdit">{{$L('重新编辑')}}</em>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="multiSelectMode && isSelectableMsg" class="dialog-multi-check" @click.stop="onMultiSelectToggle">
|
||||
<Icon :type="isSelected ? 'ios-checkmark-circle' : 'ios-radio-button-off'" :class="{checked: isSelected}"/>
|
||||
@ -182,7 +186,7 @@ export default {
|
||||
},
|
||||
|
||||
isSelectableMsg() {
|
||||
return !['tag', 'top', 'todo', 'notice', 'word-chain', 'vote', 'template'].includes(this.source.type);
|
||||
return !['tag', 'top', 'todo', 'notice', 'withdraw', 'word-chain', 'vote', 'template'].includes(this.source.type);
|
||||
},
|
||||
|
||||
classArray() {
|
||||
@ -328,6 +332,10 @@ export default {
|
||||
this.dispatch("on-merge-forward-detail", data)
|
||||
},
|
||||
|
||||
onWithdrawReEdit() {
|
||||
this.dispatch("on-withdraw-re-edit", this.source)
|
||||
},
|
||||
|
||||
dispatch(event, ...arg) {
|
||||
if (this.isReply) {
|
||||
this.$emit(event, ...arg)
|
||||
|
||||
@ -220,7 +220,8 @@
|
||||
@on-other="onOther"
|
||||
@on-show-emoji-user="onShowEmojiUser"
|
||||
@on-merge-forward-detail="onMergeForwardDetail"
|
||||
@on-multi-select-toggle="onMultiSelectToggle">
|
||||
@on-multi-select-toggle="onMultiSelectToggle"
|
||||
@on-withdraw-re-edit="onWithdrawReEdit">
|
||||
<template #header v-if="!isChildComponent">
|
||||
<div class="dialog-item head-box">
|
||||
<div v-if="loadIng > 0 || prevId > 0" class="loading" :class="{filled: allMsgs.length === 0}">
|
||||
@ -978,6 +979,7 @@ export default {
|
||||
'dialogMsgs',
|
||||
'dialogTodos',
|
||||
'dialogMsgTops',
|
||||
'dialogWithdraws',
|
||||
'dialogMsgTransfer',
|
||||
'dialogMsgKeep',
|
||||
'dialogIns',
|
||||
@ -1069,6 +1071,13 @@ export default {
|
||||
return this.tempMsgs.filter(item => item.dialog_id == this.dialogId);
|
||||
},
|
||||
|
||||
withdrawMsgList() {
|
||||
if (!this.isReady) {
|
||||
return [];
|
||||
}
|
||||
return this.dialogWithdraws.filter(item => item.dialog_id == this.dialogId);
|
||||
},
|
||||
|
||||
allMsgList() {
|
||||
if (this.isStaticMode) {
|
||||
return this.staticMsgs || []
|
||||
@ -1088,6 +1097,25 @@ export default {
|
||||
array.push(...tempMsgList)
|
||||
}
|
||||
}
|
||||
if (this.withdrawMsgList.length > 0 && !this.msgType && !this.msgId) {
|
||||
const ids = array.map(({id}) => id)
|
||||
const minId = ids.length > 0 ? Math.min(...ids) : 0
|
||||
// 撤回占位仅在已加载消息范围内显示,避免干扰向上翻页的 prev_id 判断
|
||||
const withdrawMsgList = this.withdrawMsgList
|
||||
.filter(item => !ids.includes(item.id) && (ids.length === 0 || item.id > minId))
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
dialog_id: item.dialog_id,
|
||||
prev_id: item.prev_id,
|
||||
type: 'withdraw',
|
||||
userid: this.userId,
|
||||
msg: item.msg,
|
||||
estimateSize: 42,
|
||||
}))
|
||||
if (withdrawMsgList.length > 0) {
|
||||
array.push(...withdrawMsgList)
|
||||
}
|
||||
}
|
||||
return array.sort((a, b) => {
|
||||
return a.id - b.id;
|
||||
})
|
||||
@ -3751,6 +3779,7 @@ export default {
|
||||
},
|
||||
|
||||
onWithdraw() {
|
||||
const operateItem = this.operateItem;
|
||||
$A.modalConfirm({
|
||||
content: `确定撤回此信息吗?`,
|
||||
okText: '撤回',
|
||||
@ -3760,11 +3789,22 @@ export default {
|
||||
this.$store.dispatch("call", {
|
||||
url: 'dialog/msg/withdraw',
|
||||
data: {
|
||||
msg_id: this.operateItem.id
|
||||
msg_id: operateItem.id
|
||||
},
|
||||
}).then(() => {
|
||||
resolve("消息已撤回");
|
||||
this.$store.dispatch("forgetDialogMsg", this.operateItem);
|
||||
if (operateItem.type === 'text' && $A.getObject(operateItem.msg, 'text')) {
|
||||
this.$store.dispatch("saveDialogWithdraw", {
|
||||
id: operateItem.id,
|
||||
dialog_id: operateItem.dialog_id,
|
||||
prev_id: operateItem.prev_id,
|
||||
msg: {
|
||||
type: $A.getObject(operateItem.msg, 'type'),
|
||||
text: $A.getObject(operateItem.msg, 'text'),
|
||||
},
|
||||
});
|
||||
}
|
||||
this.$store.dispatch("forgetDialogMsg", operateItem);
|
||||
}).catch(({msg}) => {
|
||||
reject(msg);
|
||||
});
|
||||
@ -3773,6 +3813,21 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
onWithdrawReEdit(source) {
|
||||
if (this.operateVisible) {
|
||||
return
|
||||
}
|
||||
this.cancelQuote()
|
||||
const {type, text} = source.msg
|
||||
if (type === 'md') {
|
||||
this.$refs.input.setText(text)
|
||||
} else {
|
||||
this.$refs.input.setContent(text.replace(/\{\{RemoteURL\}\}/g, $A.mainUrl()))
|
||||
}
|
||||
!this.windowTouch && this.inputFocus()
|
||||
this.$store.dispatch("forgetDialogWithdraw", {id: source.id})
|
||||
},
|
||||
|
||||
onViewReply(data) {
|
||||
if (this.operateVisible) {
|
||||
return
|
||||
|
||||
51
resources/assets/js/store/actions.js
vendored
51
resources/assets/js/store/actions.js
vendored
@ -7,6 +7,21 @@ import axios from "axios";
|
||||
|
||||
const dialogDraftState = { timer: {}, subTemp: null }
|
||||
|
||||
// 撤回消息本地保留(重新编辑用),过期自动清除
|
||||
const dialogWithdrawState = {
|
||||
timer: {},
|
||||
expire: 5 * 60 * 1000,
|
||||
schedule(commit, item) {
|
||||
if (this.timer[item.id]) {
|
||||
clearTimeout(this.timer[item.id])
|
||||
}
|
||||
this.timer[item.id] = setTimeout(() => {
|
||||
delete this.timer[item.id]
|
||||
commit('withdraw/remove', item.id)
|
||||
}, Math.max(0, item.time + this.expire - new Date().getTime()))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 预加载
|
||||
@ -1169,6 +1184,7 @@ export default {
|
||||
'dialogMsgs',
|
||||
'dialogDrafts',
|
||||
'dialogQuotes',
|
||||
'dialogWithdraws',
|
||||
'fileLists',
|
||||
'callAt',
|
||||
'cacheEmojis',
|
||||
@ -1212,6 +1228,15 @@ export default {
|
||||
tag: !!item.content,
|
||||
}));
|
||||
|
||||
// 特殊处理 dialogWithdraws(清除过期项,未过期的重新安排过期清除)
|
||||
const withdrawNow = new Date().getTime()
|
||||
const withdrawLength = state.dialogWithdraws.length
|
||||
state.dialogWithdraws = state.dialogWithdraws.filter(item => item.time && withdrawNow - item.time < dialogWithdrawState.expire);
|
||||
state.dialogWithdraws.forEach(item => dialogWithdrawState.schedule(commit, item));
|
||||
if (state.dialogWithdraws.length !== withdrawLength) {
|
||||
$A.IDBSave("dialogWithdraws", state.dialogWithdraws)
|
||||
}
|
||||
|
||||
// TranslationLanguage 检查
|
||||
if (typeof languageList[state.cacheTranslationLanguage] === "undefined") {
|
||||
state.cacheTranslationLanguage = languageName;
|
||||
@ -4052,6 +4077,32 @@ export default {
|
||||
commit('quote/remove', id)
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存撤回消息(仅本地,撤回后显示"重新编辑"入口)
|
||||
* @param commit
|
||||
* @param data {id, dialog_id, prev_id, msg: {type, text}, ?time}
|
||||
*/
|
||||
saveDialogWithdraw({commit}, data) {
|
||||
data = Object.assign({time: new Date().getTime()}, data)
|
||||
$A.syncDispatch("saveDialogWithdraw", data)
|
||||
commit('withdraw/set', data)
|
||||
dialogWithdrawState.schedule(commit, data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除撤回消息
|
||||
* @param commit
|
||||
* @param data {id}
|
||||
*/
|
||||
forgetDialogWithdraw({commit}, data) {
|
||||
$A.syncDispatch("forgetDialogWithdraw", data)
|
||||
if (dialogWithdrawState.timer[data.id]) {
|
||||
clearTimeout(dialogWithdrawState.timer[data.id])
|
||||
delete dialogWithdrawState.timer[data.id]
|
||||
}
|
||||
commit('withdraw/remove', data.id)
|
||||
},
|
||||
|
||||
/** *****************************************************************************************/
|
||||
/** ************************************** 消息 **********************************************/
|
||||
/** *****************************************************************************************/
|
||||
|
||||
19
resources/assets/js/store/mutations.js
vendored
19
resources/assets/js/store/mutations.js
vendored
@ -342,6 +342,25 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 撤回消息管理(仅本地,用于撤回后重新编辑)
|
||||
'withdraw/set': function(state, data) {
|
||||
const index = state.dialogWithdraws.findIndex(item => item.id === data.id)
|
||||
if (index !== -1) {
|
||||
state.dialogWithdraws.splice(index, 1, data)
|
||||
} else {
|
||||
state.dialogWithdraws.push(data)
|
||||
}
|
||||
$A.IDBSave("dialogWithdraws", state.dialogWithdraws)
|
||||
},
|
||||
|
||||
'withdraw/remove': function(state, id) {
|
||||
const index = state.dialogWithdraws.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
state.dialogWithdraws.splice(index, 1)
|
||||
$A.IDBSave("dialogWithdraws", state.dialogWithdraws)
|
||||
}
|
||||
},
|
||||
|
||||
// 长按事件
|
||||
'longpress/set': function(state, {type, data, element}) {
|
||||
state.longpressData = {type, data, element}
|
||||
|
||||
1
resources/assets/js/store/state.js
vendored
1
resources/assets/js/store/state.js
vendored
@ -162,6 +162,7 @@ export default {
|
||||
dialogHistory: [],
|
||||
dialogDrafts: [],
|
||||
dialogQuotes: [],
|
||||
dialogWithdraws: [],
|
||||
dialogMsgTransfer: {time: 0},
|
||||
dialogSseList: [],
|
||||
dialogDroupWordChain: {},
|
||||
|
||||
@ -603,7 +603,8 @@
|
||||
.dialog-tag,
|
||||
.dialog-todo,
|
||||
.dialog-top,
|
||||
.dialog-notice {
|
||||
.dialog-notice,
|
||||
.dialog-withdraw {
|
||||
font-size: 12px;
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
@ -614,6 +615,15 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.dialog-withdraw {
|
||||
.withdraw-re-edit {
|
||||
margin-left: 4px;
|
||||
font-style: normal;
|
||||
color: $primary-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-top,
|
||||
.dialog-tag {
|
||||
cursor: pointer;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user