feat: 完善 IM 群历史消息拉取与历史群前端门控
- 后端群列表返回历史群成员状态 joinStatus,用于区分当前群和历史退群群 - 群消息拉取支持基于 receiver_user_ids 快照过滤可见消息 - 补充群消息 pull、群成员候选、私聊 pull 相关索引与 SQL 脚本 - 前端接入 joinStatus,并封装历史退群群判断 - 历史退群群禁发、隐藏群操作入口,并从通讯录、转发、推荐名片候选中排除 - 保留历史群会话展示能力,用于查看退群前历史消息pull/884/MERGE
parent
89a49cf19c
commit
8c796950f9
|
|
@ -16,6 +16,7 @@ export interface ImGroupRespVO {
|
|||
dissolvedTime?: string // 解散时间
|
||||
createTime?: string // 创建时间
|
||||
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
|
||||
joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum:0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像
|
||||
}
|
||||
|
||||
// 群消息置顶 / 取消置顶 Request VO
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import { getCurrentUserId } from '@/utils/auth'
|
|||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { useFriendStore } from '../../store/friendStore'
|
||||
import { useGroupStore } from '../../store/groupStore'
|
||||
import { getMemberDisplayName } from '../../../utils/user'
|
||||
import { getMemberDisplayName, isGroupQuit } from '../../../utils/user'
|
||||
import type { Friend, GroupLite, GroupMember } from '../../types'
|
||||
import type { GroupMemberLite } from './GroupMember.vue'
|
||||
|
||||
|
|
@ -89,6 +89,10 @@ const isMember = computed(() => {
|
|||
if (!cached) {
|
||||
return false
|
||||
}
|
||||
// 历史退群群:直接判 false,避免成员未加载时误显示「进入群聊」
|
||||
if (isGroupQuit(cached)) {
|
||||
return false
|
||||
}
|
||||
if (cached.membersLoaded && cached.members) {
|
||||
const myId = getCurrentUserId()
|
||||
return cached.members.some(
|
||||
|
|
@ -97,8 +101,13 @@ const isMember = computed(() => {
|
|||
}
|
||||
return true
|
||||
})
|
||||
/** 是否未加群:有 id 但 isMember 不成立;对比 isMember 用于动作区按钮分支 */
|
||||
const isStranger = computed(() => !!props.group?.id && !isMember.value)
|
||||
/** 历史退群群:只读,动作区两个按钮都不渲染(既不「进入群聊」也不「加入群聊」) */
|
||||
const isQuitGroup = computed(() => {
|
||||
const id = props.group?.id
|
||||
return id != null && isGroupQuit(groupStore.getGroup(id))
|
||||
})
|
||||
/** 是否未加群:有 id、非成员、且非历史退群群;只有真·陌生人才给「加入群聊」 */
|
||||
const isStranger = computed(() => !!props.group?.id && !isMember.value && !isQuitGroup.value)
|
||||
|
||||
/** 成员数文案:member 优先用本地拉到的列表长度,stranger 用 props.group.memberCount 卡片快照 */
|
||||
const memberCountText = computed(() => {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ import { ImConversationType, ImMessageType, isGroupConversation } from '../../..
|
|||
import { getConversationKey } from '../../../utils/conversation'
|
||||
import { buildDefaultGroupName } from '../../../utils/group'
|
||||
import { serializeMessage, type CardTarget } from '../../../utils/message'
|
||||
import { isGroupQuit } from '../../../utils/user'
|
||||
import type { Conversation, FriendLite } from '../../types'
|
||||
|
||||
defineOptions({ name: 'ImRecommendCardDialog' })
|
||||
|
|
@ -162,9 +163,15 @@ const headerTitle = computed(() => {
|
|||
return isGroupConversation(target.value?.targetType) ? '把这个群推荐给朋友' : '把他推荐给朋友'
|
||||
})
|
||||
|
||||
/** 候选会话:从 store 拿排序后的列表(hide 由 Panel 接 hideKeys 过滤) */
|
||||
const candidateConversations = computed<Conversation[]>(
|
||||
() => conversationStore.getSortedConversationList
|
||||
/** 候选会话:从 store 拿排序后的列表(hide 由 Panel 接 hideKeys 过滤);历史退群群不可被推荐选中(选了后端也会拒) */
|
||||
const candidateConversations = computed<Conversation[]>(() =>
|
||||
conversationStore.getSortedConversationList.filter(
|
||||
(conversation) =>
|
||||
!(
|
||||
conversation.type === ImConversationType.GROUP &&
|
||||
isGroupQuit(groupStore.getGroup(conversation.targetId))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/** 隐藏 key:不能把名片推回名片本身的会话(用户名片避免自推、群名片避免推回该群) */
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { getCurrentUserId } from '@/utils/auth'
|
|||
import { useConversationStore } from '../store/conversationStore'
|
||||
import { useGroupStore } from '../store/groupStore'
|
||||
import { ImConversationType, ImGroupMemberRole } from '../../utils/constants'
|
||||
import { isGroupQuit } from '../../utils/user'
|
||||
|
||||
export type MuteOverlayInfo = { text: string; icon: string }
|
||||
|
||||
|
|
@ -61,6 +62,10 @@ export function useMuteOverlay(): ComputedRef<MuteOverlayInfo | null> {
|
|||
if (!group) {
|
||||
return null
|
||||
}
|
||||
// 历史退群群:已退群只能查看历史,禁止发送(文本 / 图片 / 文件 / 语音 / 重试共用这一层拦截)
|
||||
if (isGroupQuit(group)) {
|
||||
return { text: '你已退出群聊,仅可查看历史消息', icon: 'ant-design:logout-outlined' }
|
||||
}
|
||||
const myId = getCurrentUserId()
|
||||
// 群封禁:管理后台操作,所有人不可发送
|
||||
if (group.banned) {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ import GroupDetail from './GroupDetail.vue'
|
|||
import { useConversationStore } from '../../store/conversationStore'
|
||||
import { useFriendStore } from '../../store/friendStore'
|
||||
import { useGroupStore } from '../../store/groupStore'
|
||||
import { getFriendDisplayName, getGroupDisplayName } from '../../../utils/user'
|
||||
import { getFriendDisplayName, getGroupDisplayName, isGroupQuit } from '../../../utils/user'
|
||||
import type { FriendLite, FriendRequest, Group, GroupLite, User } from '../../types'
|
||||
import { ImConversationType } from '../../../utils/constants'
|
||||
import { StorageKeys } from '../../../utils/db'
|
||||
|
|
@ -139,14 +139,17 @@ const friendRequests = computed<FriendRequest[]>(() => friendStore.friendRequest
|
|||
const friends = computed<FriendLite[]>(() => friendStore.getActiveFriendLiteList)
|
||||
|
||||
const groups = computed<GroupLite[]>(() =>
|
||||
groupStore.groups.map((group: Group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
showGroupName: getGroupDisplayName(group), // 优先用群备注 groupRemark,没设置时回落到原群名;避免点"进入群聊"时把已同步的备注会话名刷回原名
|
||||
showImage: group.avatar,
|
||||
showImageThumb: group.avatar,
|
||||
memberCount: group.memberCount
|
||||
}))
|
||||
// 通讯录只展示当前仍在群的;已退群历史群只留在 store 里供消息展示群名 / 头像,不进通讯录
|
||||
groupStore.groups
|
||||
.filter((group: Group) => !isGroupQuit(group))
|
||||
.map((group: Group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
showGroupName: getGroupDisplayName(group), // 优先用群备注 groupRemark,没设置时回落到原群名;避免点"进入群聊"时把已同步的备注会话名刷回原名
|
||||
showImage: group.avatar,
|
||||
showImageThumb: group.avatar,
|
||||
memberCount: group.memberCount
|
||||
}))
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@
|
|||
:group-name="group.name"
|
||||
/>
|
||||
|
||||
<!-- 添加(任何成员都能邀请) -->
|
||||
<!-- 添加(任何成员都能邀请;历史退群群隐藏) -->
|
||||
<div
|
||||
v-if="!isQuitGroup"
|
||||
class="im-conversation-group-side__tile-wrap flex flex-col items-center w-[66px] cursor-pointer"
|
||||
title="邀请好友入群"
|
||||
@click="handleOpenInvite"
|
||||
|
|
@ -164,8 +165,9 @@
|
|||
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">未设置</span>
|
||||
</div>
|
||||
|
||||
<!-- 备注(仅自己可见;保存后会替换会话列表 / 顶部群名展示) -->
|
||||
<!-- 备注(仅自己可见;保存后会替换会话列表 / 顶部群名展示);历史退群群隐藏:改备注走 updateGroupMember,已退群会被后端拒 -->
|
||||
<el-popover
|
||||
v-if="!isQuitGroup"
|
||||
v-model:visible="groupRemarkPopoverVisible"
|
||||
trigger="click"
|
||||
placement="left-start"
|
||||
|
|
@ -203,8 +205,9 @@
|
|||
</div>
|
||||
</el-popover>
|
||||
|
||||
<!-- 我在本群的昵称(任何成员都能改自己的) -->
|
||||
<!-- 我在本群的昵称(任何成员都能改自己的);历史退群群隐藏:走 updateGroupMember,已退群会被后端拒 -->
|
||||
<el-popover
|
||||
v-if="!isQuitGroup"
|
||||
v-model:visible="remarkPopoverVisible"
|
||||
trigger="click"
|
||||
placement="left-start"
|
||||
|
|
@ -344,8 +347,11 @@
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 底部:退出 / 解散群聊 ==================== -->
|
||||
<div class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--el-bg-color)] border-t border-t-solid border-[var(--el-border-color-lighter)]">
|
||||
<!-- ==================== 底部:退出 / 解散群聊(历史退群群隐藏,已退群无需再退) ==================== -->
|
||||
<div
|
||||
v-if="!isQuitGroup"
|
||||
class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--el-bg-color)] border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<!-- 群主:解散群聊 -->
|
||||
<el-button
|
||||
v-if="isOwner"
|
||||
|
|
@ -410,6 +416,7 @@ import GroupOwnerTransferDialog from '../../../../components/group/GroupOwnerTra
|
|||
import GroupRequestListDialog from '../../../../components/group/GroupRequestListDialog.vue'
|
||||
import RecommendCardDialog from '../../../../components/user/RecommendCardDialog.vue'
|
||||
import { toGroupCardTarget } from '@/views/im/utils/message'
|
||||
import { isGroupQuit } from '@/views/im/utils/user'
|
||||
import type { Conversation, GroupLite } from '../../../../types'
|
||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
||||
|
||||
|
|
@ -461,12 +468,21 @@ function handleOpenInvite() {
|
|||
}
|
||||
|
||||
const myId = computed(() => getCurrentUserId())
|
||||
const isOwner = computed(() => props.group != null && props.group.ownerId === myId.value)
|
||||
/** 历史退群群:禁所有群操作入口(邀请 / 移出 / 改资料 / 禁言 / 审批 / 退出等),只保留展示;props.group 是 GroupLite 无 joinStatus,回 store 取全量 */
|
||||
const isQuitGroup = computed(() => {
|
||||
const id = props.group?.id
|
||||
return id != null && isGroupQuit(groupStore.getGroup(id))
|
||||
})
|
||||
const isOwner = computed(
|
||||
() => !isQuitGroup.value && props.group != null && props.group.ownerId === myId.value
|
||||
)
|
||||
/** 当前用户在群里的角色(来自 props.members 的 me 行);用于判定是否可移出他人 */
|
||||
const myRole = computed(() => props.members.find((m) => m.userId === myId.value)?.role)
|
||||
/** 群主或管理员:在抽屉里有 "移出群成员" 入口 */
|
||||
/** 群主或管理员:在抽屉里有 "移出群成员" 入口;历史退群群一律视为无权限 */
|
||||
const isOwnerOrAdmin = computed(
|
||||
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
||||
() =>
|
||||
!isQuitGroup.value &&
|
||||
(myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN)
|
||||
)
|
||||
|
||||
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ import Icon from '@/components/Icon/src/Icon.vue'
|
|||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { ImConversationType, ImGroupMemberRole } from '@/views/im/utils/constants'
|
||||
import { unpinGroupMessage as apiUnpinGroupMessage } from '@/api/im/group'
|
||||
import { getSenderDisplayName } from '@/views/im/utils/user'
|
||||
import { getSenderDisplayName, isGroupQuit } from '@/views/im/utils/user'
|
||||
import { resolveConversationLastContent } from '@/views/im/utils/conversation'
|
||||
import { getCurrentUserId } from '@/utils/auth'
|
||||
import { useGroupStore } from '../../../../store/groupStore'
|
||||
|
|
@ -123,6 +123,10 @@ const latest = computed(() => pinnedMessages.value[pinnedMessages.value.length -
|
|||
|
||||
/** 当前用户是否群主 / 管理员(决定是否显示「移除」入口) */
|
||||
const canManage = computed(() => {
|
||||
// 历史退群群:本地缓存残留时也不给「移除」入口
|
||||
if (isGroupQuit(group.value)) {
|
||||
return false
|
||||
}
|
||||
const myId = getCurrentUserId()
|
||||
const role = group.value?.members?.find((m) => m.userId === myId)?.role
|
||||
return role === ImGroupMemberRole.OWNER || role === ImGroupMemberRole.ADMIN
|
||||
|
|
|
|||
|
|
@ -241,7 +241,8 @@ import {
|
|||
getMemberDisplayName,
|
||||
getMentionCandidates,
|
||||
getSenderDisplayName,
|
||||
getSenderRealNickname
|
||||
getSenderRealNickname,
|
||||
isGroupQuit
|
||||
} from '@/views/im/utils/user'
|
||||
import {
|
||||
resolveFriendNotificationSegments,
|
||||
|
|
@ -811,7 +812,8 @@ const myGroupRole = computed(() => {
|
|||
|
||||
/** 是否可管理该消息发送人:我的角色高于目标角色(群主 > 管理员 > 普通成员);目标角色未知时不展示 */
|
||||
const canManageSender = computed(() => {
|
||||
if (!currentGroup.value || !myGroupRole.value) {
|
||||
// 历史退群群:本地可能残留成员缓存,显式排除,避免右键仍出「禁言 / 移除」
|
||||
if (!currentGroup.value || isGroupQuit(currentGroup.value) || !myGroupRole.value) {
|
||||
return false
|
||||
}
|
||||
const senderMember = currentGroup.value.members?.find((m) => m.userId === props.message.senderId)
|
||||
|
|
@ -856,6 +858,7 @@ function handleMultiSelectClick(e: MouseEvent) {
|
|||
const canPin = computed(
|
||||
() =>
|
||||
!!currentGroup.value &&
|
||||
!isGroupQuit(currentGroup.value) &&
|
||||
isNormalMessage(props.message.type) &&
|
||||
!!props.message.id &&
|
||||
!isRecall.value &&
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-tooltip v-else content="通话" placement="bottom">
|
||||
<el-tooltip v-else-if="!isQuitGroup" content="通话" placement="bottom">
|
||||
<Icon
|
||||
icon="ant-design:phone-outlined"
|
||||
:size="20"
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
<!-- 群通话胶囊条:仅群聊 + 该群有活跃通话时显示;点击展开看成员 + 加入按钮 -->
|
||||
<RtcGroupCallBanner
|
||||
v-if="isGroup && conversationStore.activeConversation"
|
||||
v-if="isGroup && !isQuitGroup && conversationStore.activeConversation"
|
||||
:group-id="conversationStore.activeConversation.targetId"
|
||||
/>
|
||||
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
/>
|
||||
<!-- 群顶部「待处理加群申请」横幅:仅群聊 + owner / admin + count > 0 时显示 -->
|
||||
<GroupRequestPending
|
||||
v-if="isGroup && conversationStore.activeConversation"
|
||||
v-if="isGroup && !isQuitGroup && conversationStore.activeConversation"
|
||||
:group-id="conversationStore.activeConversation.targetId"
|
||||
/>
|
||||
<!-- 私聊:对方不再是有效好友(我删了对方 / 从未加过;单边设计下「被对方删除」本端 friendStore 不更新故不会触发);胶囊嵌在 header 内(跟群置顶同级),点击弹 UserInfoCard -->
|
||||
|
|
@ -231,7 +231,7 @@ import { useMessage } from '@/hooks/web/useMessage'
|
|||
import { useConversationStore } from '../../../../store/conversationStore'
|
||||
import { useFriendStore } from '../../../../store/friendStore'
|
||||
import { useImUiStore } from '../../../../store/uiStore'
|
||||
import { getMemberDisplayName } from '@/views/im/utils/user'
|
||||
import { getMemberDisplayName, isGroupQuit } from '@/views/im/utils/user'
|
||||
import { useGroupStore } from '../../../../store/groupStore'
|
||||
import MessageItem from './MessageItem.vue'
|
||||
import MessageInput from '../input/MessageInput.vue'
|
||||
|
|
@ -321,6 +321,15 @@ const isChannel = computed(
|
|||
() => conversationStore.activeConversation?.type === ImConversationType.CHANNEL
|
||||
)
|
||||
|
||||
/** 当前激活会话是否历史退群群:禁群通话、隐藏群申请横幅等操作入口;聊天历史、群名头像照常展示 */
|
||||
const isQuitGroup = computed(() => {
|
||||
const conversation = conversationStore.activeConversation
|
||||
return (
|
||||
conversation?.type === ImConversationType.GROUP &&
|
||||
isGroupQuit(groupStore.getGroup(conversation.targetId))
|
||||
)
|
||||
})
|
||||
|
||||
/** 私聊会话且对端不是有效好友(本端 friend 记录缺失或 DISABLE);单边删除语义下「被对方删除」不触发本端横幅 */
|
||||
const showNotFriendBanner = computed(() => {
|
||||
const conversation = conversationStore.activeConversation
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ import FacePicker from '../../input/FacePicker.vue'
|
|||
import { useConversationStore } from '@/views/im/home/store/conversationStore'
|
||||
import { useFriendStore } from '@/views/im/home/store/friendStore'
|
||||
import { useGroupStore } from '@/views/im/home/store/groupStore'
|
||||
import { isGroupQuit } from '@/views/im/utils/user'
|
||||
import { useMessageSender } from '@/views/im/home/composables/useMessageSender'
|
||||
import { useMessageMultiSelect } from '@/views/im/home/composables/useMessageMultiSelect'
|
||||
import {
|
||||
|
|
@ -228,7 +229,13 @@ const confirmButtonText = computed(() =>
|
|||
/** 候选会话:从 store 拿排序后的列表(转发回原会话也允许,与微信一致);公众号 / 频道单向消息不接受转发,从候选里剔除 */
|
||||
const candidateConversations = computed<Conversation[]>(() =>
|
||||
conversationStore.getSortedConversationList.filter(
|
||||
(conversation) => conversation.type !== ImConversationType.CHANNEL
|
||||
(conversation) =>
|
||||
conversation.type !== ImConversationType.CHANNEL &&
|
||||
// 历史退群群不可被转发选中(选了后端也会拒)
|
||||
!(
|
||||
conversation.type === ImConversationType.GROUP &&
|
||||
isGroupQuit(groupStore.getGroup(conversation.targetId))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -804,7 +804,8 @@ function convertGroup(group: ImGroupRespVO): Group {
|
|||
pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO),
|
||||
mutedAll: group.mutedAll,
|
||||
banned: group.banned,
|
||||
joinApproval: group.joinApproval
|
||||
joinApproval: group.joinApproval,
|
||||
joinStatus: group.joinStatus
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ export interface Group {
|
|||
mutedAll?: boolean // 是否全群禁言
|
||||
banned?: boolean // 是否被管理员封禁
|
||||
joinApproval?: boolean // 进群是否需群主 / 管理员审批
|
||||
joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum:0 在群 / 1 已退群);历史退群群仍返回,供展示历史消息的群名 / 头像
|
||||
|
||||
// ========== 前端扩展字段(user-per-group 维度) ==========
|
||||
silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import { countBy } from 'lodash-es'
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { SystemUserSexEnum } from '@/utils/constants'
|
||||
import { CommonStatusEnum, SystemUserSexEnum } from '@/utils/constants'
|
||||
import {
|
||||
ImConversationType,
|
||||
ImFriendAddSource,
|
||||
|
|
@ -31,6 +31,15 @@ import type { Conversation, Friend, Group, User } from '../home/types'
|
|||
// MessageBubble 的 textSegments 才不会跟着无谓重算
|
||||
const EMPTY_MENTIONS: MentionCandidate[] = []
|
||||
|
||||
/**
|
||||
* 是否历史退群群:joinStatus 为 DISABLE(已退群 / 被移除)
|
||||
*
|
||||
* /im/group/list 会带回历史退群群(供展示历史消息的群名 / 头像);这类群应禁止发送、隐藏群操作入口、不可被转发 / 推荐选中
|
||||
*/
|
||||
export function isGroupQuit(group?: Group | null): boolean {
|
||||
return group?.joinStatus === CommonStatusEnum.DISABLE
|
||||
}
|
||||
|
||||
/**
|
||||
* 私聊好友显示名:备注 > 真实昵称
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue