mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-24 23:11:59 +08:00
perf(component): ⚡ 优化撤回消息重新编辑判定 (#128)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ export const useMessage = () => {
|
||||
|
||||
// 如果没有会话就把右侧消息框关闭
|
||||
if (updatedSessions.length === 0) {
|
||||
console.log('没有会话了')
|
||||
useMitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+1
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user