diff --git a/client/shared/model/group.ts b/client/shared/model/group.ts index 0d1d2d2f..5b23c249 100644 --- a/client/shared/model/group.ts +++ b/client/shared/model/group.ts @@ -394,3 +394,15 @@ export async function muteGroupMember( muteMs, }); } + +/** + * 移出群组成员 + * @param groupId 群组ID + * @param memberId 成员ID + */ +export async function deleteGroupMember(groupId: string, memberId: string) { + await request.post('/api/group/deleteGroupMember', { + groupId, + memberId, + }); +} diff --git a/client/web/src/components/Panel/group/MembersPanel.tsx b/client/web/src/components/Panel/group/MembersPanel.tsx index 1c08e421..841962e6 100644 --- a/client/web/src/components/Panel/group/MembersPanel.tsx +++ b/client/web/src/components/Panel/group/MembersPanel.tsx @@ -1,5 +1,5 @@ import { Icon } from 'tailchat-design'; -import { openReconfirmModalP } from '@/components/Modal'; +import { openReconfirmModal, openReconfirmModalP } from '@/components/Modal'; import { GroupUserPopover } from '@/components/popover/GroupUserPopover'; import { UserListItem } from '@/components/UserListItem'; import { Divider, Dropdown, Input, MenuProps, Skeleton } from 'antd'; @@ -20,6 +20,7 @@ import { useSearch, useUserInfoList, } from 'tailchat-shared'; +import _compact from 'lodash/compact'; interface MembersPanelProps { groupId: string; @@ -133,6 +134,22 @@ export const MembersPanel: React.FC = React.memo((props) => { userInfoList ); + /** + * 解除禁言 + */ + const [, handleRemoveGroupMember] = useAsyncRequest( + async (memberId: string) => { + const confirm = await openReconfirmModalP({ + title: t('确认要将该用户移出群组么'), + }); + if (confirm) { + await model.group.deleteGroupMember(groupId, memberId); + showToasts(t('操作成功'), 'success'); + } + }, + [groupId] + ); + if (userInfoList.length === 0) { return ; } @@ -141,61 +158,71 @@ export const MembersPanel: React.FC = React.memo((props) => { const hasMute = getMembersHasMute(members, member._id); if (allowManageUser) { + const muteItems: MenuProps['items'] = hasMute + ? [ + { + key: 'unmute', + label: t('解除禁言'), + onClick: () => handleUnmuteMember(member._id), + }, + ] + : [ + { + key: 'mute', + label: t('禁言'), + children: [ + { + key: '1m', + label: t('1分钟'), + onClick: () => handleMuteMember(member._id, 1 * 60 * 1000), + }, + { + key: '5m', + label: t('5分钟'), + onClick: () => handleMuteMember(member._id, 5 * 60 * 1000), + }, + { + key: '10m', + label: t('10分钟'), + onClick: () => handleMuteMember(member._id, 10 * 60 * 1000), + }, + { + key: '30m', + label: t('30分钟'), + onClick: () => handleMuteMember(member._id, 30 * 60 * 1000), + }, + { + key: '1d', + label: t('1天'), + onClick: () => + handleMuteMember(member._id, 1 * 24 * 60 * 60 * 1000), + }, + { + key: '7d', + label: t('7天'), + onClick: () => + handleMuteMember(member._id, 7 * 24 * 60 * 60 * 1000), + }, + { + key: '30d', + label: t('30天'), + onClick: () => + handleMuteMember(member._id, 30 * 24 * 60 * 60 * 1000), + }, + ], + }, + ]; + const menu: MenuProps = { - items: hasMute - ? [ - { - key: 'unmute', - label: t('解除禁言'), - onClick: () => handleUnmuteMember(member._id), - }, - ] - : [ - { - key: 'mute', - label: t('禁言'), - children: [ - { - key: '1m', - label: t('1分钟'), - onClick: () => handleMuteMember(member._id, 1 * 60 * 1000), - }, - { - key: '5m', - label: t('5分钟'), - onClick: () => handleMuteMember(member._id, 5 * 60 * 1000), - }, - { - key: '10m', - label: t('10分钟'), - onClick: () => handleMuteMember(member._id, 10 * 60 * 1000), - }, - { - key: '30m', - label: t('30分钟'), - onClick: () => handleMuteMember(member._id, 30 * 60 * 1000), - }, - { - key: '1d', - label: t('1天'), - onClick: () => - handleMuteMember(member._id, 1 * 24 * 60 * 60 * 1000), - }, - { - key: '7d', - label: t('7天'), - onClick: () => - handleMuteMember(member._id, 7 * 24 * 60 * 60 * 1000), - }, - { - key: '30d', - label: t('30天'), - onClick: () => - handleMuteMember(member._id, 30 * 24 * 60 * 60 * 1000), - }, - ], - }, - ], + items: _compact([ + ...muteItems, + { + key: 'delete', + label: t('移出群组'), + danger: true, + onClick: () => handleRemoveGroupMember(member._id), + }, + ] as MenuProps['items']), }; return ( diff --git a/client/web/src/routes/Main/Content/Group/index.tsx b/client/web/src/routes/Main/Content/Group/index.tsx index 5ea3d382..136fad9d 100644 --- a/client/web/src/routes/Main/Content/Group/index.tsx +++ b/client/web/src/routes/Main/Content/Group/index.tsx @@ -1,9 +1,9 @@ -import { LoadingSpinner } from '@/components/LoadingSpinner'; +import { Problem } from '@/components/Problem'; import { SplitPanel } from '@/components/SplitPanel'; import { GroupIdContextProvider } from '@/context/GroupIdContext'; import React from 'react'; import { Route, Routes, useParams } from 'react-router-dom'; -import { isValidStr, useGroupInfo } from 'tailchat-shared'; +import { isValidStr, t, useGroupInfo } from 'tailchat-shared'; import { PageContent } from '../PageContent'; import { GroupPanelRender, GroupPanelRoute } from './Panel'; import { GroupPanelRedirect } from './PanelRedirect'; @@ -16,7 +16,7 @@ export const Group: React.FC = React.memo(() => { const groupInfo = useGroupInfo(groupId); if (!groupInfo) { - return ; + return ; } const pinnedPanelId = groupInfo.pinnedPanelId; diff --git a/server/services/core/group/group.service.ts b/server/services/core/group/group.service.ts index 10c83ce8..db0932fc 100644 --- a/server/services/core/group/group.service.ts +++ b/server/services/core/group/group.service.ts @@ -466,19 +466,7 @@ class GroupService extends TcService { const group: Group = await this.transformDocuments(ctx, {}, doc); - // 先将自己退出房间, 然后再进行房间级别通知 - await ctx.call('gateway.leaveRoom', { - roomIds: [ - groupId, - ...group.panels - .filter((p) => p.type === GroupPanelType.TEXT) - .map((p) => p.id), - ], // 离开群组和所有面板房间 - userId, - }); - - this.unicastNotify(ctx, userId, 'remove', { groupId }); - this.notifyGroupInfoUpdate(ctx, group); + await this.memberLeaveGroup(ctx, group, userId); } } @@ -984,6 +972,12 @@ class GroupService extends TcService { const userId = ctx.meta.userId; const t = ctx.meta.t; + // 检查是否在踢自己 + if (String(memberId) === String(userId)) { + throw new Error(t('不允许踢出自己')); + } + + // 检查是否有权限 const [hasPermission] = await call(ctx).checkUserPermissions( groupId, userId, @@ -993,8 +987,10 @@ class GroupService extends TcService { throw new NoPermissionError(t('没有操作权限')); } - if (String(memberId) === String(userId)) { - throw new Error(t('不允许踢出自己')); + // 检查是否踢出了不该踢出的人 + const groupInfo = await call(ctx).getGroupInfo(groupId); + if (String(memberId) === String(groupInfo.owner)) { + throw new Error(t('不允许踢出群组OP')); } const group = await this.adapter.model.findByIdAndUpdate( @@ -1009,7 +1005,7 @@ class GroupService extends TcService { { new: true } ); - this.notifyGroupInfoUpdate(ctx, group); + await this.memberLeaveGroup(ctx, group, memberId); const memberInfo = await call(ctx).getUserInfo(memberId); await call(ctx).addGroupSystemMessage( @@ -1018,6 +1014,34 @@ class GroupService extends TcService { ); } + /** + * 退出群组流程 + * 用于踢出群组成员和主动退出群组 + * + * 先将自己退出房间, 然后再进行房间级别通知 + */ + private async memberLeaveGroup( + ctx: TcContext, + group: Group, + memberId: string + ) { + const groupId = String(group._id); + + await ctx.call('gateway.leaveRoom', { + roomIds: [ + groupId, + ...group.panels + .filter((p) => p.type === GroupPanelType.TEXT) + .map((p) => p.id), + ], // 离开群组和所有面板房间 + memberId, + }); + await Promise.all([ + this.unicastNotify(ctx, memberId, 'remove', { groupId }), + this.notifyGroupInfoUpdate(ctx, group), + ]); + } + /** * 发送通知群组信息发生变更 *