import {useCallback, useMemo, useState} from 'react' import { Dimensions, type GestureResponderEvent, type StyleProp, View, type ViewStyle, } from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useListMembersQuery} from '#/state/queries/list-members' import {useSession} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListRef} from '#/view/com/util/List' import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog' import {ListFooter} from '#/components/Lists' import * as ProfileCard from '#/components/ProfileCard' import {type app} from '#/lexicons' import type * as bsky from '#/types/bsky' const LOADING_ITEM = {kind: 'loading', _reactKey: '__loading__'} as const const EMPTY_ITEM = {kind: 'empty', _reactKey: '__empty__'} as const const ERROR_ITEM = {kind: 'error', _reactKey: '__error__'} as const const LOAD_MORE_ERROR_ITEM = { kind: 'load_more_error', _reactKey: '__load_more_error__', } as const type Item = | typeof LOADING_ITEM | typeof EMPTY_ITEM | typeof ERROR_ITEM | typeof LOAD_MORE_ERROR_ITEM | { kind: 'list_item' listItem: app.bsky.graph.defs.ListItemView } export function ListMembers({ list, style, scrollElRef, onScrolledDownChange, onPressTryAgain, renderHeader, renderEmptyState, testID, headerOffset = 0, desktopFixedHeightOffset, }: { list: string style?: StyleProp scrollElRef?: ListRef onScrolledDownChange: (isScrolledDown: boolean) => void onPressTryAgain?: () => void renderHeader: () => React.ReactElement renderEmptyState: () => React.ReactElement testID?: string headerOffset?: number desktopFixedHeightOffset?: number }) { const t = useTheme() const {_} = useLingui() const [isRefreshing, setIsRefreshing] = useState(false) const {currentAccount} = useSession() const moderationOpts = useModerationOpts() const editListsDialogControl = useDialogControl() const [selectedProfile, setSelectedProfile] = useState< bsky.profile.AnyProfileView | undefined >() const { data, isFetching, isFetched, isError, error, refetch, fetchNextPage, hasNextPage, isFetchingNextPage, } = useListMembersQuery(list) const isEmpty = !isFetching && !data?.pages.some(page => page.items.length) const isOwner = currentAccount && data?.pages[0].list.creator.did === currentAccount.did const items = useMemo(() => { const items: Item[] = [] if (isFetched) { if (isEmpty && isError) { items.push(ERROR_ITEM) } if (isEmpty) { items.push(EMPTY_ITEM) } else if (data) { for (const page of data.pages) { items.push( ...page.items.map(item => ({ kind: 'list_item' as const, listItem: item, })), ) } } if (!isEmpty && isError) { items.push(LOAD_MORE_ERROR_ITEM) } } else if (isFetching) { items.push(LOADING_ITEM) } return items }, [isFetched, isEmpty, isError, data, isFetching]) // events // = const onRefresh = useCallback(async () => { setIsRefreshing(true) try { await refetch() } catch (err) { logger.error('Failed to refresh lists', {message: err}) } setIsRefreshing(false) }, [refetch, setIsRefreshing]) const onEndReached = useCallback(async () => { if (isFetching || !hasNextPage || isError) return try { await fetchNextPage() } catch (err) { logger.error('Failed to load more lists', {message: err}) } }, [isFetching, hasNextPage, isError, fetchNextPage]) const onPressRetryLoadMore = useCallback(() => { void fetchNextPage() }, [fetchNextPage]) const onPressEditMembership = useCallback( (e: GestureResponderEvent, profile: bsky.profile.AnyProfileView) => { e.preventDefault() setSelectedProfile(profile) editListsDialogControl.open() }, [editListsDialogControl], ) // rendering // = const renderItem = useCallback( ({item}: {item: Item}) => { switch (item.kind) { case 'empty': { return renderEmptyState() } case 'error': { return ( ) } case 'load_more_error': { return ( ) } case 'loading': { return } case 'list_item': { const profile = item.listItem.subject if (!moderationOpts) return null return ( {isOwner && ( )} ) } } }, [ renderEmptyState, error, onPressTryAgain, onPressRetryLoadMore, moderationOpts, isOwner, onPressEditMembership, _, t, ], ) const renderFooter = useCallback(() => { if (isEmpty) return null return ( ) }, [ hasNextPage, error, isFetchingNextPage, fetchNextPage, isEmpty, headerOffset, ]) return ( item.kind === 'list_item' ? item.listItem.subject.did : item._reactKey } renderItem={renderItem} ListHeaderComponent={!isEmpty ? renderHeader : undefined} ListFooterComponent={renderFooter} refreshing={isRefreshing} onRefresh={() => void onRefresh()} headerOffset={headerOffset} contentContainerStyle={{ minHeight: Dimensions.get('window').height * 1.5, }} onScrolledDownChange={onScrolledDownChange} onEndReached={() => void onEndReached()} onEndReachedThreshold={0.6} removeClippedSubviews={true} desktopFixedHeight={desktopFixedHeightOffset || true} /> ) }