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) <noreply@anthropic.com>
This commit is contained in:
@@ -7,18 +7,22 @@ import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-disp
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group'
|
||||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||||
import {useRequireAuth, useSession} from '#/state/session'
|
import {useRequireAuth, useSession} from '#/state/session'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {
|
import {
|
||||||
type ConvoWithDetails,
|
type ConvoWithDetails,
|
||||||
type GroupConvoMember,
|
type GroupConvoMember,
|
||||||
} from '#/components/dms/util'
|
} from '#/components/dms/util'
|
||||||
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
|
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {MemberMenu} from './MemberMenu'
|
import {MemberMenu} from './MemberMenu'
|
||||||
|
import {RemoveMemberPrompt} from './prompts'
|
||||||
import {StatusBadge} from './StatusBadge'
|
import {StatusBadge} from './StatusBadge'
|
||||||
import {SubtleHoverWrapper} from './SubtleHoverWrapper'
|
import {SubtleHoverWrapper} from './SubtleHoverWrapper'
|
||||||
|
|
||||||
@@ -45,6 +49,14 @@ export function Member({
|
|||||||
const [queueFollow] = useProfileFollowMutationQueue(profile, 'GroupChat')
|
const [queueFollow] = useProfileFollowMutationQueue(profile, 'GroupChat')
|
||||||
const requireAuth = useRequireAuth()
|
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 isFollowing = !!profile.viewer?.following
|
||||||
|
|
||||||
const handleFollow = () => {
|
const handleFollow = () => {
|
||||||
@@ -101,6 +113,9 @@ export function Member({
|
|||||||
)}`
|
)}`
|
||||||
: l`Added by invite link`
|
: l`Added by invite link`
|
||||||
|
|
||||||
|
// Surface a prominent remove button to the owner for blocked members.
|
||||||
|
const showRemoveButton = isOwner && !isSelf && !!isBlockedOrBlocking(profile)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubtleHoverWrapper>
|
<SubtleHoverWrapper>
|
||||||
<View style={outerStyles}>
|
<View style={outerStyles}>
|
||||||
@@ -131,13 +146,29 @@ export function Member({
|
|||||||
web(a.pt_2xs),
|
web(a.pt_2xs),
|
||||||
]}>
|
]}>
|
||||||
{joinedReason}
|
{joinedReason}
|
||||||
|
{showRemoveButton && (
|
||||||
|
<>
|
||||||
|
{' • '}
|
||||||
|
<Trans>Blocked</Trans>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ProfileCard.Header>
|
</ProfileCard.Header>
|
||||||
</ProfileCard.Outer>
|
</ProfileCard.Outer>
|
||||||
</ProfileCard.Link>
|
</ProfileCard.Link>
|
||||||
{isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : (
|
{showRemoveButton ? (
|
||||||
|
<Button
|
||||||
|
label={l`Remove ${displayName} from this group chat`}
|
||||||
|
size="tiny"
|
||||||
|
color="negative_subtle"
|
||||||
|
onPress={() => removeMemberPrompt.open()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Remove</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
) : isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : (
|
||||||
<SimpleInlineLinkText
|
<SimpleInlineLinkText
|
||||||
label={l`Follow ${displayName}`}
|
label={l`Follow ${displayName}`}
|
||||||
{...createStaticClick(handleFollow)}
|
{...createStaticClick(handleFollow)}
|
||||||
@@ -147,6 +178,11 @@ export function Member({
|
|||||||
)}
|
)}
|
||||||
{statusBadge}
|
{statusBadge}
|
||||||
</View>
|
</View>
|
||||||
|
<RemoveMemberPrompt
|
||||||
|
control={removeMemberPrompt}
|
||||||
|
displayName={displayName}
|
||||||
|
onConfirm={() => removeMembers({members: [profile.did]})}
|
||||||
|
/>
|
||||||
</SubtleHoverWrapper>
|
</SubtleHoverWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
|
|
||||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -213,6 +214,12 @@ function GroupSettings({
|
|||||||
const bIsSelf = b.did === currentAccount?.did
|
const bIsSelf = b.did === currentAccount?.did
|
||||||
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
|
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
|
||||||
if (aIsSelf !== bIsSelf) return aIsSelf ? -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
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Prompt.Basic
|
||||||
|
control={control}
|
||||||
|
title={l`Remove ${displayName}?`}
|
||||||
|
description={l`They won’t be able to rejoin unless you invite them again.`}
|
||||||
|
confirmButtonCta={l`Remove`}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
cancelButtonCta={l`Cancel`}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function BlockMemberPrompt({
|
export function BlockMemberPrompt({
|
||||||
control,
|
control,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
|||||||
Reference in New Issue
Block a user