import {useCallback, useMemo, useState} from 'react' import {useWindowDimensions, View} from 'react-native' import {type AppBskyGraphDefs, type ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { getMembership, type ListMembersip, useDangerousListMembershipsQuery, useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships' import {useMyListsQuery} from '#/state/queries/my-lists' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, 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 ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' export function UserAddRemoveListsDialog({ control, profile, onChange, }: { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView onChange?: ( type: 'add' | 'remove', list: AppBskyGraphDefs.ListView, ) => void | undefined }) { const {height} = useWindowDimensions() return ( ) } const LOADING = {type: 'loading', _reactKey: '__loading__'} as const const EMPTY = {type: 'empty', _reactKey: '__empty__'} as const const ERROR_ITEM = {type: 'error', _reactKey: '__error__'} as const type ListItem = | typeof LOADING | typeof EMPTY | typeof ERROR_ITEM | { type: 'section-header' title: string _reactKey: string } | { type: 'list' list: AppBskyGraphDefs.ListView } function DialogInner({ profile, onChange, }: { profile: bsky.profile.AnyProfileView onChange?: ( type: 'add' | 'remove', list: AppBskyGraphDefs.ListView, ) => void | undefined }) { const t = useTheme() const {_} = useLingui() const control = Dialog.useDialogContext() const moderationOpts = useModerationOpts() const [headerHeight, setHeaderHeight] = useState(50) const {data, isPending, isError, error} = useMyListsQuery('all') const {data: memberships} = useDangerousListMembershipsQuery() const isEmpty = !isPending && !data?.length const items = useMemo(() => { let _items: ListItem[] = [] if (isError && isEmpty) { _items = _items.concat([ERROR_ITEM]) } if (isPending || !moderationOpts) { _items = _items.concat([LOADING]) } else if (isEmpty) { _items = _items.concat([EMPTY]) } else if (data) { // TODO: Add starter packs! -sfn const curateLists = data.filter( list => list.purpose === 'app.bsky.graph.defs#curatelist', ) const modLists = data.filter( list => list.purpose === 'app.bsky.graph.defs#modlist', ) if (curateLists.length > 0) { _items = _items.concat( { type: 'section-header', title: _(msg`User lists`), _reactKey: 'curatelist', }, curateLists.map(list => ({type: 'list', list})), ) } if (modLists.length > 0) { _items = _items.concat( { type: 'section-header', title: _(msg`Moderation lists`), _reactKey: 'modlist', }, modLists.map(list => ({type: 'list', list})), ) } } return _items }, [isError, isEmpty, isPending, moderationOpts, data, _]) const renderItem = useCallback( ({item}: {item: ListItem}) => { switch (item.type) { case 'empty': { return ( You have no lists. ) } case 'error': return case 'loading': return ( ) case 'section-header': return ( {item.title} ) case 'list': return ( ) } return null }, [t, error, memberships, onChange, profile, moderationOpts], ) const renderCloseButton = useCallback( () => ( ), [control, _], ) return ( setHeaderHeight(e.nativeEvent.layout.height)} renderRight={renderCloseButton}> Add{' '} {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), )}{' '} to lists } /> ) } function keyExtractor(item: ListItem) { return item.type === 'list' ? item.list.uri : item._reactKey } function ListRow({ profile, list, memberships, onChange, moderationOpts, }: { profile: bsky.profile.AnyProfileView list: AppBskyGraphDefs.ListView memberships: ListMembersip[] | undefined onChange?: ( type: 'add' | 'remove', list: AppBskyGraphDefs.ListView, ) => void | undefined moderationOpts?: ModerationOpts }) { const t = useTheme() const {_} = useLingui() const membership = useMemo( () => getMembership(memberships, list.uri, profile.did), [memberships, list.uri, profile.did], ) const {mutate: listMembershipAdd, isPending: isAddingPending} = useListMembershipAddMutation({ onSuccess: () => { Toast.show(_(msg`Added to list`)) onChange?.('add', list) }, onError: e => Toast.show(cleanError(e), 'xmark'), }) const {mutate: listMembershipRemove, isPending: isRemovingPending} = useListMembershipRemoveMutation({ onSuccess: () => { Toast.show(_(msg`Removed from list`)) onChange?.('remove', list) }, onError: e => Toast.show(cleanError(e), 'xmark'), }) const isMutating = isAddingPending || isRemovingPending const onToggleMembership = useCallback(() => { if (typeof membership === 'undefined') { return } if (membership === false) { listMembershipAdd({ listUri: list.uri, actorDid: profile.did, }) } else { listMembershipRemove({ listUri: list.uri, actorDid: profile.did, membershipUri: membership, }) } }, [list, profile, membership, listMembershipAdd, listMembershipRemove]) if (!moderationOpts) return null return ( {sanitizeDisplayName(list.name)} {list.purpose === 'app.bsky.graph.defs#curatelist' && ( User list )} {list.purpose === 'app.bsky.graph.defs#modlist' && ( Moderation list )} {membership !== undefined && ( )} ) }