群成员格子改用头像外角标 ⊖ 移出,替换上一版的行内文字按钮
上一版把 ➖ 加在了"查看全部"弹层的成员行文字按钮里,用户纠正:应该是紧凑头像格子 (群聊设置面板里"查看全部"之前那一屏)里,像"+邀请"那样在头像外侧显示一个独立的 角标,而不是列表行内的文字。canRemoveMember 挪到 lib/api/groups.ts 共享,避免 两处判断逻辑分叉。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2d589cbe6b
commit
05ee23aefa
@@ -12,6 +12,7 @@ import { GroupAnnouncementModal } from '@/features/groups/GroupAnnouncementModal
|
||||
import { GroupFilesModal } from '@/features/groups/GroupFilesModal';
|
||||
import { GroupMembersModal } from '@/features/groups/GroupMembersModal';
|
||||
import {
|
||||
canRemoveMember,
|
||||
disbandGroup,
|
||||
getGroup,
|
||||
getMyGroupProfile,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
getGroupSettings,
|
||||
getGroupSummary,
|
||||
leaveGroup,
|
||||
removeGroupMember,
|
||||
updateGroup,
|
||||
updateGroupMemberSettings,
|
||||
updateMyGroupNickname,
|
||||
@@ -71,6 +73,28 @@ function RoleCorner({ role }: { role: GroupMember['role'] }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Corner "-" badge for a removable member's avatar in the compact grid -- the "outside the avatar,
|
||||
* like the +邀请 tile" treatment (explicit user correction: a text "移出" link inside a member ROW is
|
||||
* the full-list modal's job, not this grid's). Opposite corner from RoleCorner (top-right, not
|
||||
* top-left) because both can legitimately apply to the same avatar at once -- an owner removing an
|
||||
* admin still needs the "管" tag AND this badge on one avatar. */
|
||||
function RemoveBadge({ pending, onRemove }: { pending: boolean; onRemove: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
aria-label="移出群聊"
|
||||
className="absolute -right-1 -top-1 z-10 grid h-4 w-4 place-items-center rounded-full bg-danger-text text-white shadow-sm disabled:opacity-60 md:h-[18px] md:w-[18px]"
|
||||
>
|
||||
<Icon name="remove" className="text-[10px]" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* README §4.4's 群聊设置 detail rail (group-owner/admin/member view). Sibling of DetailColumn.tsx
|
||||
* (the P2P equivalent) -- same layout conventions, but group-specific data source (lib/api/groups.ts,
|
||||
@@ -183,6 +207,19 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
|
||||
onError: (err) => showError(err, '群名保存失败'),
|
||||
});
|
||||
|
||||
// Drives the corner "-" badge on the compact member grid below -- same removeGroupMember call and
|
||||
// same invalidation keys as GroupMembersModal's own remove button (canRemoveMember is now shared
|
||||
// from lib/api/groups.ts so the two surfaces can't drift on who's eligible to remove whom).
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (userId: string) => removeGroupMember(topicId, userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['group-members', topicId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['group', topicId] });
|
||||
},
|
||||
onError: (err) => showError(err, '移出失败,请重试'),
|
||||
});
|
||||
|
||||
// Both mutations call forgetTopic() the instant their own REST call succeeds, rather than waiting
|
||||
// on the async native/`group.dissolved` gone-signal that normally follows a group actually going
|
||||
// away -- real, user-reported bugs fixed by this: (1) the chat list kept showing a broken stale
|
||||
@@ -372,6 +409,17 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
|
||||
className="h-11 w-11 rounded-lg text-sm md:h-10 md:w-10"
|
||||
/>
|
||||
<RoleCorner role={m.role} />
|
||||
{canRemoveMember(myUserId, myRole || 'member', m) ? (
|
||||
<RemoveBadge
|
||||
pending={removeMutation.isPending && removeMutation.variables === m.user_id}
|
||||
onRemove={async () => {
|
||||
const label = m.display_name || m.user_id;
|
||||
if (await requestConfirm({ message: `确定将"${label}"移出群聊吗?`, danger: true })) {
|
||||
removeMutation.mutate(m.user_id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span className="w-full truncate text-center text-[11px] text-text-secondary">{m.display_name || m.user_id}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from '@/app/Icon';
|
||||
import { Toast } from '@/app/Toast';
|
||||
import { fetchAllPages } from '@/lib/api/client';
|
||||
import { getFriends } from '@/lib/api/friends';
|
||||
import { getAllGroupMembers, inviteGroupMembers, removeGroupMember, type GroupInvitationResult, type GroupRole } from '@/lib/api/groups';
|
||||
import { canRemoveMember, getAllGroupMembers, inviteGroupMembers, removeGroupMember, type GroupInvitationResult, type GroupRole } from '@/lib/api/groups';
|
||||
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
||||
import { useEscapeKey } from '@/lib/hooks';
|
||||
import { requestConfirm } from '@/stores/confirmDialog';
|
||||
@@ -64,15 +64,6 @@ const OUTCOME_LABEL: Record<InviteOutcome, string> = {
|
||||
failed: '邀请失败',
|
||||
};
|
||||
|
||||
/** Mirrors server/im_http.go's canRemoveIMGroupMember exactly: can't remove yourself or the owner;
|
||||
* removing a plain member needs admin-or-better, removing an admin needs the owner specifically. */
|
||||
function canRemoveMember(myUserId: string | null, myRole: GroupRole, member: { user_id: string; role: GroupRole }): boolean {
|
||||
if (!myUserId || member.user_id === myUserId) return false;
|
||||
if (myRole === 'member') return false;
|
||||
if (member.role === 'owner') return false;
|
||||
if (member.role === 'admin' && myRole !== 'owner') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initialView = 'members', onClose }: GroupMembersModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -213,6 +213,18 @@ export function removeGroupMember(topic: string, userId: string) {
|
||||
return imDelete<{ topic: string; user_id: string; status: string }>(`/groups/${encodeURIComponent(topic)}/members/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
/** Mirrors server/im_http.go's canRemoveIMGroupMember exactly: can't remove yourself or the owner;
|
||||
* removing a plain member needs admin-or-better, removing an admin needs the owner specifically.
|
||||
* Shared by every surface that renders a per-member remove affordance (GroupMembersModal's full
|
||||
* list, GroupDetailColumn's compact avatar grid) so the rule can't drift between the two. */
|
||||
export function canRemoveMember(myUserId: string | null, myRole: GroupRole, member: { user_id: string; role: GroupRole }): boolean {
|
||||
if (!myUserId || member.user_id === myUserId) return false;
|
||||
if (myRole === 'member') return false;
|
||||
if (member.role === 'owner') return false;
|
||||
if (member.role === 'admin' && myRole !== 'owner') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface GroupMemberProfile {
|
||||
topic: string;
|
||||
user_id: string;
|
||||
|
||||
Reference in New Issue
Block a user