From 3fdfebfe29c070f3687ea9980c44db87ed0f757b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 20:15:03 +0300 Subject: [PATCH] [Chat] Surface blocked members to top of group member list for owner (#10726) Co-authored-by: Claude Opus 4.8 (1M context) --- .../Messages/ConversationSettings/Member.tsx | 35 ++++++++++++++++++- .../ConversationSettings/MemberMenu.tsx | 10 ++++-- .../Messages/ConversationSettings/index.tsx | 7 ++++ .../Messages/ConversationSettings/prompts.tsx | 24 +++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 9786e69f55..20cc0fee52 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 ( @@ -137,7 +152,17 @@ export function Member({ - {isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + {showRemoveButton ? ( + + ) : isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + {/* Mounted outside the showRemoveButton conditional: confirming the + prompt optimistically drops this row, so gating the prompt on the + button would unmount it mid-close and race the dismiss animation. */} + removeMembers({members: [profile.did]})} + /> ) } diff --git a/src/screens/Messages/ConversationSettings/MemberMenu.tsx b/src/screens/Messages/ConversationSettings/MemberMenu.tsx index aee0cbf54e..2d36bc115f 100644 --- a/src/screens/Messages/ConversationSettings/MemberMenu.tsx +++ b/src/screens/Messages/ConversationSettings/MemberMenu.tsx @@ -26,7 +26,7 @@ import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' -import {BlockMemberPrompt} from './prompts' +import {BlockMemberPrompt, RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' export function MemberMenu({ @@ -50,6 +50,7 @@ export function MemberMenu({ const requireEmailVerification = useRequireEmailVerification() const blockMemberPrompt = Prompt.usePromptControl() + const removeMemberPrompt = Prompt.usePromptControl() const [menuDidOpen, setMenuDidOpen] = useState(false) const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did, { @@ -227,7 +228,7 @@ export function MemberMenu({ removeMembers({members: [profile.did]})}> + onPress={removeMemberPrompt.open}> Remove from chat @@ -241,6 +242,11 @@ export function MemberMenu({ control={blockMemberPrompt} onConfirm={() => void handleBlockMember()} /> + removeMembers({members: [profile.did]})} + /> ) } diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index b4c7e1ce03..2aca54dfe0 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -11,6 +11,7 @@ import {useNavigation} from '@react-navigation/native' import {HITSLOP_10} from '#/lib/constants' 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, @@ -214,6 +215,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,