import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isNetworkError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import { useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships' import { type ListWithMembership, removeListMembershipOptimistically, updateListMembershipOptimistically, useListsWithMembershipQuery, } from '#/state/queries/lists-with-membership' import {useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {BulletList_Stroke2_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' export type UserAddRemoveListsDialogProps = { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView | undefined onAdd?: (listUri: string) => void onRemove?: (listUri: string) => void } export function UserAddRemoveListsDialog({ control, profile, onAdd, onRemove, }: UserAddRemoveListsDialogProps) { return ( ) } function Empty() { const t = useTheme() return ( You have no lists. ) } function ListsContent({ profile, onAdd, onRemove, }: { profile: bsky.profile.AnyProfileView | undefined onAdd?: (listUri: string) => void onRemove?: (listUri: string) => void }) { const t = useTheme() const control = Dialog.useDialogContext() const {t: l} = useLingui() const {gtMobile} = useBreakpoints() const { data, isError, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage, } = useListsWithMembershipQuery({actor: profile?.did}) const listItems = data?.pages.flatMap(page => page.listsWithMembership) || [] const onEndReached = async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() } catch (err) { // Error handling is optional since this is just pagination } } const renderItem = ({item}: {item: ListWithMembership}) => profile ? ( ) : null const onClose = () => { control.close() } const listHeader = ( {profile ? ( Update {createSanitizedDisplayName(profile, true)} in Lists ) : ( Update in Lists )} ) return ( ( ) : renderItem } keyExtractor={ isLoading ? () => 'lists_dialog_loader' : (item: ListWithMembership) => item.list.uri } onEndReached={() => void onEndReached()} onEndReachedThreshold={0.1} stickyHeaderIndices={web([0])} ListHeaderComponent={listHeader} ListFooterComponent={ isFetchingNextPage ? ( ) : undefined } ListEmptyComponent={!isLoading && data ? : undefined} webInnerContentContainerStyle={[a.py_0]} style={platform({ web: [a.px_2xl, a.pb_md], native: [a.px_2xl, a.pt_lg], })} /> ) } function ListItem({ listWithMembership, profile, onAdd, onRemove, }: { listWithMembership: ListWithMembership profile: bsky.profile.AnyProfileView onAdd?: (listUri: string) => void onRemove?: (listUri: string) => void }) { const {t: l} = useLingui() const t = useTheme() const queryClient = useQueryClient() const {currentAccount} = useSession() const list = listWithMembership.list const listItem = listWithMembership.listItem const isMember = !!listItem const {mutate: addMembership, isPending: isPendingAdd} = useListMembershipAddMutation({ subject: profile, onSuccess: data => { Toast.show(l`Added to list`) onAdd?.(list.uri) updateListMembershipOptimistically({ queryClient, actor: profile.did, listUri: list.uri, membershipUri: data.uri, subject: { did: profile.did, handle: profile.handle, displayName: profile.displayName, }, }) }, onError: err => { if (!isNetworkError(err)) { logger.error('Failed to add to list', {safeMessage: err}) } Toast.show(l`Failed to add to list`, {type: 'error'}) }, }) const {mutate: removeMembership, isPending: isPendingRemove} = useListMembershipRemoveMutation({ onSuccess: () => { Toast.show(l`Removed from list`) onRemove?.(list.uri) removeListMembershipOptimistically({ queryClient, actor: profile.did, listUri: list.uri, }) }, onError: err => { if (!isNetworkError(err)) { logger.error('Failed to remove from list', {safeMessage: err}) } Toast.show(l`Failed to remove from list`, {type: 'error'}) }, }) const isPending = isPendingAdd || isPendingRemove const handleToggleMembership = () => { if (isPending) return if (!isMember) { addMembership({ listUri: list.uri, actorDid: profile.did, }) } else { if (!listItem?.uri) { logger.error('Cannot remove: missing membership URI') return } removeMembership({ listUri: list.uri, actorDid: profile.did, membershipUri: listItem.uri, }) } } return ( {sanitizeDisplayName(list.name)} {list.purpose === 'app.bsky.graph.defs#curatelist' && (list.creator.did === currentAccount?.did ? ( User list by you ) : ( User list by {sanitizeHandle(list.creator.handle, '@')} ))} {list.purpose === 'app.bsky.graph.defs#modlist' && (list.creator.did === currentAccount?.did ? ( Moderation list by you ) : ( Moderation list by {sanitizeHandle(list.creator.handle, '@')} ))} ) }