import {useMemo, useState} from 'react' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' import {moderateProfile} from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {type NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {List} from '#/view/com/util/List' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' import type * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {useInteractionState} from '#/components/hooks/useInteractionState' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft' import { Bell2_Stroke2_Corner0_Rounded as BellIcon, Bell2Off_Stroke2_Corner0_Rounded as BellOffIcon, } from '#/components/icons/Bell2' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron' import {type Props as SVGIconProps} from '#/components/icons/common' import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' import {EditBig_Stroke2_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message' import { Person_Stroke2_Corner2_Rounded as PersonIcon, PersonX_Stroke2_Corner0_Rounded as PersonXIcon, } from '#/components/icons/Person' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import * as Menu from '#/components/Menu' import {type TriggerChildProps} from '#/components/Menu/types' import * as Prompt from '#/components/Prompt' import {SubtleHover} from '#/components/SubtleHover' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import type * as bsky from '#/types/bsky' const MEMBER_LIMIT = 50 const ROW_SPACING = 10 type Item = | { type: 'MEMBERS_AND_REQUESTS' } | { type: 'ADD_MEMBERS_LINK' } | { type: 'CHAT_MEMBER' profile: bsky.profile.AnyProfileView status: 'admin' | 'member' | 'invited' } /** * TODO This is just layout for now. */ export function MessagesConversationSettingsScreen() { const {gtTablet} = useBreakpoints() return ( Group chat settings ) } function keyExtractor(item: Item) { return item.type === 'CHAT_MEMBER' ? item.profile.did : item.type } function SettingsInner() { const initialNumToRender = useInitialNumToRender({minItemHeight: 68}) const bottomBarOffset = useBottomBarOffset() const data: bsky.profile.AnyProfileView[] = [] const invites: string[] = [] const items = [ { type: 'MEMBERS_AND_REQUESTS', }, { type: 'ADD_MEMBERS_LINK', }, ...data.map((profile, index) => ({ type: 'CHAT_MEMBER', profile, status: index === 0 ? 'admin' : invites.includes(profile.did) ? 'invited' : 'member', })), ] function renderItem({item}: {item: Item}) { switch (item.type) { case 'MEMBERS_AND_REQUESTS': return case 'ADD_MEMBERS_LINK': return case 'CHAT_MEMBER': return default: return null } } return ( } renderItem={renderItem} sideBorders={false} windowSize={11} onEndReachedThreshold={IS_NATIVE ? 1.5 : 0} /> ) } function MembersAndRequests({ memberCount, requestCount, }: { memberCount: number requestCount: number }) { const t = useTheme() const {t: l} = useLingui() return ( Members{' '} {l`${memberCount}/${MEMBER_LIMIT}`} {l`${plural(requestCount, { one: '# request', other: '# requests', })}`} ) } function AddMembersLink() { const t = useTheme() return ( [ a.flex_row, a.align_center, a.justify_between, pressed && web({outline: 'none'}), ]}> {({pressed}) => ( <> Add members )} ) } function Member({ profile, status, }: { profile: bsky.profile.AnyProfileView status: 'admin' | 'member' | 'invited' }) { const navigation = useNavigation() const t = useTheme() const {t: l} = useLingui() const moderationOpts = useModerationOpts() const moderation = useMemo( () => moderationOpts ? moderateProfile(profile, moderationOpts) : undefined, [profile, moderationOpts], ) if (!moderation) return null const invitedByDisplayName = 'Darrin Loeliger' const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount ? l`Deleted Account` : sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), ) let invitedBy: React.ReactNode | null = ( {l`Added by ${invitedByDisplayName}`} ) let statusBadge: React.ReactNode | null = ( ) switch (status) { case 'admin': invitedBy = null statusBadge = break case 'invited': invitedBy = ( {l`Invited by ${invitedByDisplayName}`} ) statusBadge = break } return ( { navigation.navigate('Profile', {name: profile.did}) }}> {displayName} {sanitizeHandle(profile.handle, '@')} {invitedBy ? ( {invitedBy} ) : null} {statusBadge} ) } function StatusBadge({label}: {label: string}) { const t = useTheme() return ( {label} ) } function StatusButton({ label, style, ...rest }: { label: string style?: StyleProp } & TriggerChildProps['props']) { const t = useTheme() return ( {label} ) } function MemberMenu({ profile, type, }: { profile: bsky.profile.AnyProfileView type: 'member' | 'invited' }) { const navigation = useNavigation() const t = useTheme() const {t: l} = useLingui() const ax = useAnalytics() const requireEmailVerification = useRequireEmailVerification() const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did) const {mutate: initiateConvo} = useGetConvoForMembers({ onSuccess: ({convo}) => { ax.metric('chat:open', {logContext: 'ProfileHeader'}) navigation.navigate('MessagesConversation', {conversation: convo.id}) }, onError: () => { Toast.show(l`Failed to create conversation`) }, }) const onPress = () => { if (!convoAvailability?.canChat) { return } if (convoAvailability.convo) { ax.metric('chat:open', {logContext: 'ProfileHeader'}) navigation.navigate('MessagesConversation', { conversation: convoAvailability.convo.id, }) } else { ax.metric('chat:create', {logContext: 'ProfileHeader'}) initiateConvo([profile.did]) } } const wrappedOnPress = requireEmailVerification(onPress, { instructions: [ Before you can message another user, you must first verify your email. , ], }) const moderationOpts = useModerationOpts() const moderation = useMemo( () => moderationOpts ? moderateProfile(profile, moderationOpts) : undefined, [profile, moderationOpts], ) if (!moderation) return null const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount ? l`Deleted Account` : sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), ) return ( {({props, state}) => type === 'invited' ? ( ) : ( ) } { navigation.navigate('Profile', {name: profile.did}) }}> Go to profile Message {type === 'member' ? ( <> {}}> Block {}}> Remove from chat ) : null} {type === 'invited' ? ( {}}> Uninvite ) : null} ) } function SettingsHeader({profiles}: {profiles: bsky.profile.AnyProfileView[]}) { const t = useTheme() const {t: l} = useLingui() const editNamePrompt = Prompt.usePromptControl() const inviteLinkPrompt = Prompt.usePromptControl() const lockChatPrompt = Prompt.usePromptControl() const [groupName, setGroupName] = useState('Work in Progress') const [newGroupName, setNewGroupName] = useState(groupName) const [isMuted, setIsMuted] = useState(false) const [isLocked, setIsLocked] = useState(false) const handleToggleMute = () => { setIsMuted(prev => !prev) } const handlePromptName = () => { editNamePrompt.open() } const handleEditName = () => { setGroupName(newGroupName) editNamePrompt.close() } const handlePromptInviteLink = () => { inviteLinkPrompt.open() } const handleConfirmInviteLink = () => { inviteLinkPrompt.close() } const handlePromptLock = () => { lockChatPrompt.open() } const handleConfirmLock = () => { setIsLocked(true) } const handleUnlock = () => { setIsLocked(false) } return ( <> {groupName} Created April 2, 2026 ) } function SettingsButton({ color = 'secondary', icon, label, text, onPress, }: { color?: ButtonColor icon: React.ComponentType label: string text: string onPress: () => void }) { const t = useTheme() return ( {text} ) } type EditNamePromptProps = { control: Dialog.DialogOuterProps['control'] value: string onChangeText: (value: string) => void onConfirm: () => void } function EditNamePrompt({ control, value, onChangeText, onConfirm, }: EditNamePromptProps) { const {t: l} = useLingui() return ( <> Edit group name ) } function InviteLinkPrompt({ control, onConfirm, }: { control: Dialog.DialogOuterProps['control'] onConfirm: () => void }) { const {t: l} = useLingui() return ( ) } function LockChatPrompt({ control, onConfirm, }: { control: Dialog.DialogOuterProps['control'] onConfirm: () => void }) { const {t: l} = useLingui() return ( ) } function SubtleHoverWrapper({children}: React.PropsWithChildren) { const { state: hover, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() return ( {children} ) }