import { computed, type ComputedRef, onScopeDispose, ref } from 'vue'
|
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
|
import { ImConversationType, ImGroupMemberRole } from '../../utils/constants'
|
import { isGroupQuit } from '../../utils/user'
|
import { useConversationStore } from '../store/conversationStore'
|
import { useGroupStore } from '../store/groupStore'
|
|
export type MuteOverlayInfo = { icon: string; text: string; }
|
|
/**
|
* 模块级共享 now tick + 订阅计数:
|
* MessageItem v-for 时每条消息都会 useMuteOverlay(),单实例自起 timer 会变成几百个 30s 定时器;
|
* 改成模块级共享后所有订阅者共用一份 setInterval,订阅数清零时也清掉 timer,避免内存与时钟漂移
|
*/
|
const sharedNow = ref(Date.now())
|
let sharedTickTimer: null | number = null
|
let subscriberCount = 0
|
|
function subscribeNowTick(): void {
|
subscriberCount++
|
if (!sharedTickTimer) {
|
sharedTickTimer = window.setInterval(() => {
|
sharedNow.value = Date.now()
|
}, 30_000)
|
}
|
}
|
|
function unsubscribeNowTick(): void {
|
subscriberCount--
|
if (subscriberCount <= 0) {
|
subscriberCount = 0
|
if (sharedTickTimer) {
|
window.clearInterval(sharedTickTimer)
|
sharedTickTimer = null
|
}
|
}
|
}
|
|
/**
|
* 当前激活会话的禁言 / 封禁状态;非群聊或无禁言时返回 null
|
*
|
* 优先级:封禁 > 全群禁言(群主 / 管理员豁免)> 成员禁言
|
*
|
* MessageInput 顶部 overlay / handleResend 重试 / uploadAndSendMedia 上传完前都共用一份,
|
* 避免「输入框拦了但重试绕过」「上传期间被禁言但 sendRaw 仍发出去」之类的不一致
|
*/
|
export function useMuteOverlay(): ComputedRef<MuteOverlayInfo | null> {
|
const conversationStore = useConversationStore()
|
const groupStore = useGroupStore()
|
|
// 订阅模块级 tick;scope 销毁时反订阅,最后一个订阅者退场后 timer 也跟着清
|
subscribeNowTick()
|
onScopeDispose(unsubscribeNowTick)
|
|
return computed(() => {
|
const conversation = conversationStore.activeConversation
|
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
return null
|
}
|
const group = groupStore.getGroup(conversation.targetId)
|
if (!group) {
|
return null
|
}
|
// 历史退群群:已退群只能查看历史,禁止发送(文本 / 图片 / 文件 / 语音 / 重试共用这一层拦截)
|
if (isGroupQuit(group)) {
|
return { text: '你已退出群聊,仅可查看历史消息', icon: 'ant-design:logout-outlined' }
|
}
|
const myId = getCurrentUserId()
|
// 群封禁:管理后台操作,所有人不可发送
|
if (group.banned) {
|
return { text: '该群已被管理员封禁,无法发送消息', icon: 'ant-design:stop-outlined' }
|
}
|
// 全群禁言:群主走 ownerUserId 比较直接豁免;其它人需要成员列表加载完才能区分管理员 vs 普通成员
|
// - 加载完 + 我是管理员 → 豁免
|
// - 加载完 + 我不是管理员(含已退群)→ 拦
|
// - 加载未完 → 不显示 overlay,后端兜底拒绝普通成员;避免误拦管理员
|
if (group.mutedAll && myId !== group.ownerUserId && group.membersLoaded) {
|
const myMember = group.members?.find((m) => m.userId === myId)
|
if (myMember?.role !== ImGroupMemberRole.ADMIN) {
|
return { text: '全群禁言中,暂时无法发送消息', icon: 'ant-design:audio-muted-outlined' }
|
}
|
}
|
// 成员禁言:muteEndTime 在未来才算;用响应式 sharedNow 比对,到期后下一个 tick 就让 overlay 消失
|
const myMember = group.members?.find((m) => m.userId === myId)
|
if (myMember?.muteEndTime) {
|
const endTime = new Date(myMember.muteEndTime)
|
if (endTime.getTime() > sharedNow.value) {
|
const pad = (n: number) => n.toString().padStart(2, '0')
|
const timeStr =
|
`${pad(endTime.getMonth() + 1)}-${pad(endTime.getDate())} ` +
|
`${pad(endTime.getHours())}:${pad(endTime.getMinutes())}`
|
return {
|
text: `您已被禁言,解除时间:${timeStr}`,
|
icon: 'ant-design:audio-muted-outlined'
|
}
|
}
|
}
|
return null
|
})
|
}
|