gaoluyang
昨天 b64a0deae5b5d33f9e20671a68936b27f0b9b00b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
  })
}