perf(component): ⚡ 优化撤回消息重新编辑判定 (#128)

This commit is contained in:
Dawn
2024-12-28 02:37:32 +08:00
committed by GitHub
parent 77373e3782
commit 451ded82c5
12 changed files with 140 additions and 35 deletions
-6
View File
@@ -43,12 +43,6 @@ jobs:
- name: Run tests with coverage
run: pnpm coverage
- name: Upload coverage to Codecov
uses: ./
with:
file: ./coverage/coverage-final.json
flags: unittest
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
with:
+35 -10
View File
@@ -16,8 +16,37 @@
</n-flex>
<Transition name="chat-init" appear mode="out-in" @after-leave="handleTransitionComplete">
<!-- 初次加载的骨架屏 -->
<n-flex
v-if="messageOptions?.isLoading && !messageOptions?.cursor"
vertical
:size="18"
style="max-height: calc(100vh - 260px)"
class="relative h-100vh box-border p-20px">
<n-flex justify="end">
<n-skeleton style="border-radius: 14px" height="40px" width="46%" :sharp="false" />
<n-skeleton height="40px" circle />
</n-flex>
<n-flex>
<n-skeleton height="40px" circle />
<n-skeleton style="border-radius: 14px" height="60px" width="58%" :sharp="false" />
</n-flex>
<n-flex>
<n-skeleton height="40px" circle />
<n-skeleton style="border-radius: 14px" height="40px" width="26%" :sharp="false" />
</n-flex>
<n-flex justify="end">
<n-skeleton style="border-radius: 14px" height="40px" width="60%" :sharp="false" />
<n-skeleton height="40px" circle />
</n-flex>
</n-flex>
<!-- 中间聊天内容(使用虚拟列表) -->
<VirtualList
v-else
id="image-chat-main"
ref="virtualListInst"
:items="chatMessageList"
@@ -366,7 +395,7 @@ import { usePopover } from '@/hooks/usePopover.ts'
import { useWindow } from '@/hooks/useWindow.ts'
import { useChatMain } from '@/hooks/useChatMain.ts'
import { delay } from 'lodash-es'
import { formatTimestamp, isDiffNow } from '@/utils/ComputedTime.ts'
import { formatTimestamp } from '@/utils/ComputedTime.ts'
import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
import { useChatStore } from '@/stores/chat.ts'
import { type } from '@tauri-apps/plugin-os'
@@ -713,15 +742,8 @@ const canReEdit = computed(() => (msgId: number) => {
const message = chatStore.getMessage(msgId)
if (!recalledMsg || !message) return false
// 判断是否是当前用户的撤回消息且在2分钟内
return (
message.fromUser.uid === userUid.value &&
!isDiffNow({
time: recalledMsg.recallTime,
unit: 'minute',
diff: 2
})
)
// 只需要判断是否是当前用户的消息,时间检查已经在 getRecalledMessage 中处理
return message.fromUser.uid === userUid.value
})
const handleReEdit = (msgId: number) => {
@@ -767,6 +789,9 @@ onUnmounted(() => {
}
hoverBubble.value.key = -1
window.removeEventListener('click', closeMenu, true)
// 清理所有过期定时器
chatStore.clearAllExpirationTimers()
})
</script>
+5 -4
View File
@@ -302,16 +302,17 @@ onMounted(async () => {
background-color: var(--emoji-hover);
cursor: pointer;
}
&.active {
background-color: var(--emoji-active-color) !important;
}
&:last-child {
margin-right: 0;
}
}
}
.active {
background-color: #13987f;
}
.emoji-content {
position: relative;
width: 100%;
+10
View File
@@ -214,6 +214,7 @@ export const useCommon = () => {
const divNode = document.createElement('div')
divNode.id = 'replyDiv' // 设置id为replyDiv
divNode.contentEditable = 'false' // 设置为不可编辑
divNode.tabIndex = -1 // 防止被focus
divNode.style.cssText = `
background-color: var(--reply-bg);
font-size: 12px;
@@ -223,7 +224,9 @@ export const useCommon = () => {
border-radius: 8px;
margin-bottom: 2px;
user-select: none;
pointer-events: none; /* 防止鼠标事件 */
cursor: default;
outline: none; /* 移除focus时的轮廓 */
`
// 把dom中的value值作为回复信息的作者,dom中的content作为回复信息的内容
const author = dom.accountName + ':'
@@ -244,6 +247,8 @@ export const useCommon = () => {
padding: 0 4px;
color: rgba(19, 152, 127);
cursor: default;
user-select: none;
pointer-events: none;
`
headerNode.appendChild(document.createTextNode(author))
// 创建一个div标签节点包裹正文内容
@@ -276,6 +281,7 @@ export const useCommon = () => {
border-radius: 4px;
cursor: default;
user-select: none;
pointer-events: none;
`
// 将img标签节点插入到div标签节点中
divNode.appendChild(contentBox)
@@ -298,6 +304,8 @@ export const useCommon = () => {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
pointer-events: none;
`
contentBox.appendChild(document.createTextNode(content))
}
@@ -312,6 +320,8 @@ export const useCommon = () => {
cursor: pointer;
margin-left: 10px;
flex-shrink: 0;
user-select: none;
pointer-events: auto; /* 确保关闭按钮可以点击 */
`
closeBtn.textContent = '关闭'
closeBtn.addEventListener('click', () => {
+3 -3
View File
@@ -20,9 +20,9 @@ export const useLogin = () => {
const logout = async () => {
const { createWebviewWindow } = useWindow()
// todo 退出账号 需要关闭其他的全部窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', false, 320, 448).then(() => {
emit(EventEnum.LOGOUT)
emitTo('tray', 'logout_success')
await createWebviewWindow('登录', 'login', 320, 448, 'home', false, 320, 448).then(async () => {
await emit(EventEnum.LOGOUT)
await emitTo('tray', 'logout_success')
})
}
-1
View File
@@ -124,7 +124,6 @@ export const useMessage = () => {
// 如果没有会话就把右侧消息框关闭
if (updatedSessions.length === 0) {
console.log('没有会话了')
useMitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
return
}
+2 -1
View File
@@ -24,6 +24,7 @@ import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
import { useUserStore } from '@/stores/user.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { AvatarUtils } from '@/utils/avatarUtils.ts'
const formRef = ref<FormInst | null>()
const formValue = ref({
@@ -83,7 +84,7 @@ export const LockScreen = defineComponent(() => {
<NFlex vertical justify="center" align="center" size={20}>
<span class="text-(14px center)">锁定屏幕</span>
<NAvatar bordered round size={80} src={userStore.userInfo.avatar} />
<NAvatar bordered round size={80} src={AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)} />
<p class="text-(14px center [--text-color]) truncate w-200px">{userStore.userInfo.name}</p>
</NFlex>
+47 -6
View File
@@ -13,11 +13,20 @@ import { useUserStore } from '@/stores/user.ts'
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
import { sendNotification } from '@tauri-apps/plugin-notification'
type RecalledMessage = {
messageId: number
content: string
recallTime: number
}
// 定义每页加载的消息数量
export const pageSize = 20
// 标识是否第一次请求
let isFirstInit = false
// 撤回消息的过期时间
const RECALL_EXPIRATION_TIME = 2 * 60 * 1000 // 2分钟,单位毫秒
export const useChatStore = defineStore(
'chat',
() => {
@@ -43,7 +52,7 @@ export const useChatStore = defineStore(
) // 消息加载状态
const replyMapping = reactive<Map<number, Map<number, number[]>>>(new Map([[currentRoomId.value, new Map()]])) // 回复消息的映射关系
// 存储撤回的消息内容和时间
const recalledMessages = reactive<Map<number, { content: string; recallTime: number }>>(new Map())
const recalledMessages = reactive<Map<number, RecalledMessage>>(new Map())
// 当前聊天室的消息Map计算属性
const currentMessageMap = computed({
@@ -334,7 +343,7 @@ export const useChatStore = defineStore(
})
}
// tab 在后台获得新消息,就开始闪烁!
// tab 在台获得新消息,就开始闪烁!
// if (document.hidden && !shakeTitle.isShaking) {
// shakeTitle.start()
// }
@@ -403,17 +412,32 @@ export const useChatStore = defineStore(
}
}
// 存储每条撤回消息的过期定时器
const expirationTimers = new Map<number, number>()
// 更新消息撤回状态
const updateRecallStatus = (data: RevokedMsgType) => {
const { msgId } = data
const message = currentMessageMap.value?.get(msgId)
if (message && typeof data.recallUid === 'number') {
// 存储撤回的消息内容和时间
const recallTime = Date.now()
recalledMessages.set(msgId, {
messageId: msgId,
content: message.message.body.content,
recallTime: Date.now()
recallTime
})
// 为这条消息设置过期定时器 TODO: setTimeout会有精度问题
const timeoutId = window.setTimeout(() => {
recalledMessages.delete(msgId)
expirationTimers.delete(msgId)
triggerMessageMapUpdate()
}, RECALL_EXPIRATION_TIME)
// 存储定时器ID以便清理
expirationTimers.set(msgId, timeoutId)
message.message.type = MsgEnum.RECALL
const cacheUser = cachedStore.userCachedList[data.recallUid]
// 如果撤回者的 id 不等于消息发送人的 id, 或者你本人就是管理员,那么显示管理员撤回的。
@@ -436,11 +460,27 @@ export const useChatStore = defineStore(
}
}
// 获取撤回的消息内容
const getRecalledMessage = (msgId: number) => {
// 添加一个工具函数来触发消息列表更新
const triggerMessageMapUpdate = () => {
if (currentMessageMap.value) {
const newMap = new Map(currentMessageMap.value)
currentMessageMap.value = newMap
}
}
// 获取撤回消息
const getRecalledMessage = (msgId: number): RecalledMessage | undefined => {
return recalledMessages.get(msgId)
}
// 清理所有定时器
const clearAllExpirationTimers = () => {
expirationTimers.forEach((timerId) => {
clearTimeout(timerId)
})
expirationTimers.clear()
}
// 删除消息
const deleteMsg = (msgId: number) => {
currentMessageMap.value?.delete(msgId)
@@ -526,7 +566,8 @@ export const useChatStore = defineStore(
getMessage,
removeContact,
getRecalledMessage,
recalledMessages
recalledMessages,
clearAllExpirationTimers
}
},
{
+3 -1
View File
@@ -8,7 +8,7 @@ html,
body,
svg {
outline: none;
overscroll-behavior: none; // 禁止mac的“触底反弹”效果
overscroll-behavior: none; // 禁止mac的"触底反弹"效果
}
/**! 主题变量 */
/* todo: 这里只需要定义单个颜色的变量即可无需重复定义组件的颜色 */
@@ -68,6 +68,7 @@ svg {
// 表情框样式
--bg-emoji: rgba(255, 255, 255, 0.6);
--emoji-hover: #e3e3e3;
--emoji-active-color: #13987f;
// 回复消息样式
--reply-color: #909090;
--reply-hover: #505050;
@@ -149,6 +150,7 @@ html[data-theme='dark'] {
// 表情框样式
--bg-emoji: rgba(27, 27, 27, 0.8);
--emoji-hover: #3b3b3b;
--emoji-active-color: #13987f;
// 回复消息样式
--reply-color: #e3e3e3;
--reply-hover: #b1b1b1;
+1
View File
@@ -54,6 +54,7 @@ declare module 'vue' {
NPopselect: typeof import('naive-ui')['NPopselect']
NQrCode: typeof import('naive-ui')['NQrCode']
NRadio: typeof import('naive-ui')['NRadio']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- // TODO 锁屏时隐藏其他窗口,解锁后再显示 (nyh -> 2024-07-14 01:39:01) -->
<div
data-tauri-drag-region
class="lock-bg rounded-8px select-none absolute top-0 left-0 w-full h-full z-9999 transition-all duration-300 ease-in-out">
class="lock-bg overflow-y-hidden rounded-8px select-none absolute top-0 left-0 w-full h-full z-9999 transition-all duration-300 ease-in-out">
<ActionBar class="absolute top-0 right-0 z-99999" :current-label="appWindow.label" :shrink="false" />
<Transition name="slide-fade" appear>
+33 -2
View File
@@ -2,7 +2,7 @@
<!-- 消息列表 // TODO 使用虚拟列表组件就用不了动画和拖动了 (nyh -> 2024-03-28 06:01:00) -->
<n-scrollbar ref="msg-scrollbar" style="max-height: calc(100vh - 70px)">
<!-- 右键菜单组件 -->
<div class="p-[4px_10px_0px_8px]">
<div v-if="sessionList.length > 0" class="p-[4px_10px_0px_8px]">
<ContextMenu
v-for="item in sessionList"
:key="item.roomId"
@@ -47,6 +47,37 @@
</n-flex>
</ContextMenu>
</div>
<!-- 加载中显示的骨架屏 -->
<n-flex
v-else-if="chatStore.sessionOptions.isLoading"
vertical
:size="18"
style="max-height: calc(100vh - 70px)"
class="relative h-100vh box-border p-20px">
<n-flex>
<n-skeleton style="border-radius: 14px" height="60px" width="100%" :sharp="false" />
</n-flex>
<n-flex>
<n-skeleton style="border-radius: 14px" height="40px" width="80%" :sharp="false" />
</n-flex>
<n-flex justify="end">
<n-skeleton style="border-radius: 14px" height="40px" width="80%" :sharp="false" />
</n-flex>
<n-flex>
<n-skeleton style="border-radius: 14px" height="60px" width="100%" :sharp="false" />
</n-flex>
</n-flex>
<!-- 没有数据显示的提示 -->
<n-result v-else class="absolute-center" status="418" description="快和朋友聊天吧!">
<template #footer>
<n-button secondary type="primary">寻找好友</n-button>
</template>
</n-result>
</n-scrollbar>
</template>
<script lang="ts" setup>
@@ -60,7 +91,7 @@ import { useUserInfo } from '@/hooks/useCached.ts'
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
import { useCommon } from '@/hooks/useCommon.ts'
import SysNTF from '@/components/common/SystemNotification.tsx'
import { AvatarUtils } from '@/utils/avatarUtils'
import { AvatarUtils } from '@/utils/avatarUtils.ts'
import { useGroupStore } from '@/stores/group.ts'
const chatStore = useChatStore()