import {useState} from 'react' import {View} from 'react-native' import { type ChatBskyConvoDefs, ChatBskyConvoLeaveConvo, ChatBskyGroupRemoveMembers, } from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/types' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import { createListMutualGroupsQueryKey, useListMutualGroupsQuery, } from '#/state/queries/messages/list-mutual-groups' import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group' import {useSession} from '#/state/session' import {atoms as a, native, useTheme, web} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog' import {parseConvoView} from '#/components/dms/util' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {type AnyProfileView} from '#/types/bsky/profile' type Item = ChatBskyConvoDefs.ConvoView type BlockDialogProps = { control: DialogControlProps profile: Shadow onBlock: () => Promise currentConvoId?: string } export function BlockDialog({ control, profile, onBlock, currentConvoId, }: BlockDialogProps) { return ( ) } function BlockDialogInner({ control, profile, onBlock, currentConvoId, }: { control: DialogControlProps profile: Shadow onBlock: () => Promise currentConvoId?: string }) { const t = useTheme() const {t: l} = useLingui() const [headerHeight, setHeaderHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0) /* * Optimistically hide convos the viewer has left or removed the profile * from, before the query refetches. We don't expect many items here, so a * simple filter is fine. */ const [removedConvoIds, setRemovedConvoIds] = useState>( () => new Set(), ) const onOptimisticallyRemoveConvo = (convoId: string) => { setRemovedConvoIds(prev => { const next = new Set(prev) next.add(convoId) return next }) } const onRestoreConvo = (convoId: string) => { setRemovedConvoIds(prev => { const next = new Set(prev) next.delete(convoId) return next }) } const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} = useListMutualGroupsQuery({ subject: profile.did, enabled: !profile.viewer?.blocking, }) const items: Item[] = (data?.pages.flatMap(page => page.convos) ?? []).filter( item => !removedConvoIds.has(item.id), ) const hasMutualGroupChats = items.length > 0 const onEndReached = async () => { if (isFetchingNextPage || !hasNextPage) return try { await fetchNextPage() } catch (err) { logger.error('Failed to load more mutual group chats', {message: err}) } } const renderItems = ({item}: {item: Item}) => { return ( ) } const listHeader = ( setHeaderHeight(evt.nativeEvent.layout.height)}> {profile.viewer?.blocking ? ( Unblock account? ) : ( Block account? )} {profile.viewer?.blocking ? ( The account will be able to interact with you after unblocking. ) : profile.associated?.labeler ? ( Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you. ) : ( Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. )} {hasMutualGroupChats ? ( Mutual group chats ) : null} ) const footer = ( ) if (isLoading || !hasMutualGroupChats) { return ( {listHeader} {footer} ) } return ( ) : null } footer={ setFooterHeight(evt.nativeEvent.layout.height)}> {footer} } contentContainerStyle={[a.gap_0, {paddingBottom: footerHeight}]} scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} onEndReached={() => void onEndReached()} onEndReachedThreshold={0.5} style={[web([{height: '100vh', maxHeight: 600}])]} webInnerStyle={[{maxWidth: 420}]} /> ) } function MutualGroupChat({ view, profileDid, currentConvoId, onOptimisticallyRemoveConvo, onRestoreConvo, }: { view: ChatBskyConvoDefs.ConvoView profileDid: string currentConvoId?: string onOptimisticallyRemoveConvo: (convoId: string) => void onRestoreConvo: (convoId: string) => void }) { const t = useTheme() const {t: l} = useLingui() const {currentAccount} = useSession() const queryClient = useQueryClient() const convo = parseConvoView(view, currentAccount?.did) const {mutate: leaveConvo, isPending: isLeavePending} = useLeaveConvo( convo?.view.id, { onSuccess: () => { Toast.show(l`Left group chat.`) void queryClient.invalidateQueries({ queryKey: createListMutualGroupsQueryKey({subject: profileDid}), }) }, onError: error => { onRestoreConvo(view.id) logger.error('Error leaving group chat', {message: error}) let errorMessage = l`Could not leave chat.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` } else if (error instanceof ChatBskyConvoLeaveConvo.InvalidConvoError) { errorMessage = l`Chat not found.` } else if ( error instanceof ChatBskyConvoLeaveConvo.OwnerCannotLeaveError ) { errorMessage = l`Chat owners cannot leave a group chat.` } Toast.show(errorMessage, {type: 'error'}) }, }, ) const {mutate: removeMembers, isPending: isRemovePending} = useRemoveFromGroupChat(convo?.view.id, { onSuccess: () => { Toast.show(l`Member removed from group chat.`) void queryClient.invalidateQueries({ queryKey: createListMutualGroupsQueryKey({subject: profileDid}), }) }, onError: error => { onRestoreConvo(view.id) logger.error('Error removing group chat member', {message: error}) let errorMessage = l`Could not remove member.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` } else if ( error instanceof ChatBskyGroupRemoveMembers.InvalidConvoError ) { errorMessage = l`Chat not found.` } else if ( error instanceof ChatBskyGroupRemoveMembers.InsufficientRoleError ) { errorMessage = l`You must be a chat owner to remove a member.` } Toast.show(errorMessage, {type: 'error'}) }, }) if (!convo || convo.kind !== 'group') return null const owner = convo.primaryMember const isViewerOwner = owner?.did != null && owner.did === currentAccount?.did const isProfileOwner = owner?.did != null && owner.did === profileDid const isCurrentConvo = view.id === currentConvoId return ( {convo.details.name} {isViewerOwner ? ( You own this chat ) : isProfileOwner ? ( They own this chat ) : null} {isViewerOwner ? ( ) : isCurrentConvo ? ( Current chat ) : ( )} ) }