From 3188bd9f2bc21ce66b44fcc1b1623b62c48f8648 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 4 Jun 2026 16:19:02 +0300 Subject: [PATCH] surface blocked members to top of group chat member list for owner Add an owner-only sort tier so blocked-or-blocking members appear at the top of the member list, a "Blocked" indicator on those rows, and an inline Remove button (with confirmation prompt) to kick them. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Messages/ConversationSettings/Member.tsx | 38 ++++++++++++++++++- .../Messages/ConversationSettings/index.tsx | 7 ++++ .../Messages/ConversationSettings/prompts.tsx | 24 ++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 9786e69f55..ab39f11741 100644 --- a/src/screens/Messages/ConversationSettings/Member.tsx +++ b/src/screens/Messages/ConversationSettings/Member.tsx @@ -7,18 +7,22 @@ import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-disp import {logger} from '#/logger' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useRequireAuth, useSession} from '#/state/session' import {atoms as a, native, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import { type ConvoWithDetails, type GroupConvoMember, } from '#/components/dms/util' import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' +import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {MemberMenu} from './MemberMenu' +import {RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' import {SubtleHoverWrapper} from './SubtleHoverWrapper' @@ -45,6 +49,14 @@ export function Member({ const [queueFollow] = useProfileFollowMutationQueue(profile, 'GroupChat') const requireAuth = useRequireAuth() + const removeMemberPrompt = Prompt.usePromptControl() + const {mutate: removeMembers} = useRemoveFromGroupChat(convo.view.id, { + onError: e => { + logger.error('Failed to remove group chat member', {message: e}) + Toast.show(l`Failed to remove group chat member`, {type: 'error'}) + }, + }) + const isFollowing = !!profile.viewer?.following const handleFollow = () => { @@ -101,6 +113,9 @@ export function Member({ )}` : l`Added by invite link` + // Surface a prominent remove button to the owner for blocked members. + const showRemoveButton = isOwner && !isSelf && !!isBlockedOrBlocking(profile) + return ( @@ -131,13 +146,29 @@ export function Member({ web(a.pt_2xs), ]}> {joinedReason} + {showRemoveButton && ( + <> + {' • '} + Blocked + + )} )} - {isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + {showRemoveButton ? ( + + ) : isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + removeMembers({members: [profile.did]})} + /> ) } diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 2ae09b5130..cd182762be 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -10,6 +10,7 @@ import {useNavigation} from '@react-navigation/native' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -213,6 +214,12 @@ function GroupSettings({ const bIsSelf = b.did === currentAccount?.did if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1 + // Surface blocked members to the owner so they can be removed. + if (isOwner) { + const aBlocked = !!isBlockedOrBlocking(a) + const bBlocked = !!isBlockedOrBlocking(b) + if (aBlocked !== bBlocked) return aBlocked ? -1 : 1 + } return 0 }) diff --git a/src/screens/Messages/ConversationSettings/prompts.tsx b/src/screens/Messages/ConversationSettings/prompts.tsx index 2eb293bdef..0c3ba2e8ca 100644 --- a/src/screens/Messages/ConversationSettings/prompts.tsx +++ b/src/screens/Messages/ConversationSettings/prompts.tsx @@ -151,6 +151,30 @@ export function LeaveAndLockChatPrompt({ ) } +export function RemoveMemberPrompt({ + control, + displayName, + onConfirm, +}: { + control: Dialog.DialogOuterProps['control'] + displayName: string + onConfirm: () => void +}) { + const {t: l} = useLingui() + + return ( + + ) +} + export function BlockMemberPrompt({ control, onConfirm,