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,