import {View} from 'react-native' import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 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' const outerStyles = [a.px_xl, a.py_sm, a.flex_row, a.align_center, a.gap_sm] export function Member({ convo, profile: profileUnshadowed, status, isOwner, }: { convo: ConvoWithDetails profile: GroupConvoMember status: 'owner' | 'standard' isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() const profile = useProfileShadow(profileUnshadowed) const {currentAccount} = useSession() const moderationOpts = useModerationOpts() 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 = () => { requireAuth(async () => { try { await queueFollow() Toast.show(l`Following ${displayName}`) } catch (err) { const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to follow', {message: String(e)}) Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } } }) } if (!moderationOpts) { return } const moderation = moderateProfile(profile, moderationOpts) const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount ? l`Deleted Account` : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) const isProfileOwner = profile.did === convo.primaryMember?.did const isSelf = currentAccount?.did === profile.did let statusBadge: React.ReactNode | null = null if (isSelf) { if (status === 'owner') { statusBadge = } } else { statusBadge = ( ) } const joinedReason = profile.kind?.addedBy ? l`Added by ${createSanitizedDisplayName( profile.kind.addedBy, true, moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'), )}` : l`Added by invite link` // Surface a prominent remove button to the owner for blocked members. const showRemoveButton = isOwner && !isSelf && !!isBlockedOrBlocking(profile) return ( {!isProfileOwner && ( {joinedReason} )} {showRemoveButton ? ( ) : isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( Follow )} {statusBadge} {/* 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]})} /> ) } export function MemberPlaceholder() { return ( ) }