import React, {useCallback, useMemo} from 'react' import { ActivityIndicator, FlatList, Pressable, StyleSheet, View, } from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {useNavigation} from '@react-navigation/native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {observer} from 'mobx-react-lite' import {RichText as RichTextAPI} from '@atproto/api' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {Feed} from 'view/com/posts/Feed' import {Text} from 'view/com/util/text/Text' import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' import {CenteredView} from 'view/com/util/Views' import {EmptyState} from 'view/com/util/EmptyState' import {RichText} from 'view/com/util/text/RichText' import {Button} from 'view/com/util/forms/Button' import {TextLink} from 'view/com/util/Link' import * as Toast from 'view/com/util/Toast' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' import {Haptics} from 'lib/haptics' import {ListModel} from 'state/models/content/list' import {PostsFeedModel} from 'state/models/feeds/posts' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {NavigationProp} from 'lib/routes/types' import {toShareUrl} from 'lib/strings/url-helpers' import {shareUrl} from 'lib/sharing' import {resolveName} from 'lib/api' import {s} from 'lib/styles' import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink, makeListLink} from 'lib/routes/links' import {ComposeIcon2} from 'lib/icons' import {ListItems} from 'view/com/lists/ListItems' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] interface SectionRef { scrollToTop: () => void } type Props = NativeStackScreenProps export const ProfileListScreen = withAuthRequired( observer(function ProfileListScreenImpl(props: Props) { const pal = usePalette('default') const store = useStores() const navigation = useNavigation() const {name: handleOrDid} = props.route.params const [listOwnerDid, setListOwnerDid] = React.useState() const [error, setError] = React.useState() const onPressBack = useCallback(() => { if (navigation.canGoBack()) { navigation.goBack() } else { navigation.navigate('Home') } }, [navigation]) React.useEffect(() => { /* * We must resolve the DID of the list owner before we can fetch the list. */ async function fetchDid() { try { const did = await resolveName(store, handleOrDid) setListOwnerDid(did) } catch (e) { setError( `We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, ) } } fetchDid() }, [store, handleOrDid, setListOwnerDid]) if (error) { return ( Could not load list {error} ) } return listOwnerDid ? ( ) : ( ) }), ) export const ProfileListScreenInner = observer( function ProfileListScreenInnerImpl({ route, listOwnerDid, }: Props & {listOwnerDid: string}) { const store = useStores() const {rkey} = route.params const feedSectionRef = React.useRef(null) const aboutSectionRef = React.useRef(null) const list: ListModel = useMemo(() => { const model = new ListModel( store, `at://${listOwnerDid}/app.bsky.graph.list/${rkey}`, ) return model }, [store, listOwnerDid, rkey]) const feed = useMemo( () => new PostsFeedModel(store, 'list', {list: list.uri}), [store, list], ) useSetTitle(list.data?.name) useFocusEffect( useCallback(() => { store.shell.setMinimalShellMode(false) list.loadMore(true).then(() => { if (list.isCuratelist) { feed.setup() } }) }, [store, list, feed]), ) const onPressAddUser = useCallback(() => { store.shell.openModal({ name: 'list-add-user', list, onAdd() { if (list.isCuratelist) { feed.refresh() } }, }) }, [store, list, feed]) const onCurrentPageSelected = React.useCallback( (index: number) => { if (index === 0) { feedSectionRef.current?.scrollToTop() } if (index === 1) { aboutSectionRef.current?.scrollToTop() } }, [feedSectionRef], ) const renderHeader = useCallback(() => { return
}, [rkey, list]) if (list.isCuratelist) { return ( {({onScroll, headerHeight, isScrolledDown}) => ( )} {({onScroll, headerHeight, isScrolledDown}) => ( )} store.shell.openComposer({})} icon={ } accessibilityRole="button" accessibilityLabel="New post" accessibilityHint="" /> ) } if (list.isModlist) { return ( {({onScroll, headerHeight, isScrolledDown}) => ( )} store.shell.openComposer({})} icon={ } accessibilityRole="button" accessibilityLabel="New post" accessibilityHint="" /> ) } return
}, ) const Header = observer(function HeaderImpl({ rkey, list, }: { rkey: string list: ListModel }) { const pal = usePalette('default') const palInverted = usePalette('inverted') const store = useStores() const navigation = useNavigation() const onTogglePinned = useCallback(async () => { Haptics.default() list.togglePin().catch(e => { Toast.show('There was an issue contacting the server') store.log.error('Failed to toggle pinned list', {e}) }) }, [store, list]) const onSubscribeMute = useCallback(() => { store.shell.openModal({ name: 'confirm', title: 'Mute these accounts?', message: 'Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.', confirmBtnText: 'Mute this List', async onPressConfirm() { try { await list.mute() Toast.show('List muted') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } }, onPressCancel() { store.shell.closeModal() }, }) }, [store, list]) const onUnsubscribeMute = useCallback(async () => { try { await list.unmute() Toast.show('List unmuted') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } }, [list]) const onSubscribeBlock = useCallback(() => { store.shell.openModal({ name: 'confirm', title: 'Block these accounts?', message: 'Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.', confirmBtnText: 'Block this List', async onPressConfirm() { try { await list.block() Toast.show('List blocked') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } }, onPressCancel() { store.shell.closeModal() }, }) }, [store, list]) const onUnsubscribeBlock = useCallback(async () => { try { await list.unblock() Toast.show('List unblocked') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } }, [list]) const onPressEdit = useCallback(() => { store.shell.openModal({ name: 'create-or-edit-list', list, onSave() { list.refresh() }, }) }, [store, list]) const onPressDelete = useCallback(() => { store.shell.openModal({ name: 'confirm', title: 'Delete List', message: 'Are you sure?', async onPressConfirm() { await list.delete() Toast.show('List deleted') if (navigation.canGoBack()) { navigation.goBack() } else { navigation.navigate('Home') } }, }) }, [store, list, navigation]) const onPressReport = useCallback(() => { if (!list.data) return store.shell.openModal({ name: 'report', uri: list.uri, cid: list.data.cid, }) }, [store, list]) const onPressShare = useCallback(() => { const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`) shareUrl(url) }, [list.creatorDid, rkey]) const dropdownItems: DropdownItem[] = useMemo(() => { if (!list.hasLoaded) { return [] } let items: DropdownItem[] = [ { testID: 'listHeaderDropdownShareBtn', label: 'Share', onPress: onPressShare, icon: { ios: { name: 'square.and.arrow.up', }, android: '', web: 'share', }, }, ] if (list.isOwner) { items.push({label: 'separator'}) items.push({ testID: 'listHeaderDropdownEditBtn', label: 'Edit List Details', onPress: onPressEdit, icon: { ios: { name: 'pencil', }, android: '', web: 'pen', }, }) items.push({ testID: 'listHeaderDropdownDeleteBtn', label: 'Delete List', onPress: onPressDelete, icon: { ios: { name: 'trash', }, android: '', web: ['far', 'trash-can'], }, }) } else { items.push({label: 'separator'}) items.push({ testID: 'listHeaderDropdownReportBtn', label: 'Report List', onPress: onPressReport, icon: { ios: { name: 'exclamationmark.triangle', }, android: '', web: 'circle-exclamation', }, }) } return items }, [ list.hasLoaded, list.isOwner, onPressShare, onPressEdit, onPressDelete, onPressReport, ]) const subscribeDropdownItems: DropdownItem[] = useMemo(() => { return [ { testID: 'subscribeDropdownMuteBtn', label: 'Mute accounts', onPress: onSubscribeMute, icon: { ios: { name: 'speaker.slash', }, android: '', web: 'user-slash', }, }, { testID: 'subscribeDropdownBlockBtn', label: 'Block accounts', onPress: onSubscribeBlock, icon: { ios: { name: 'person.fill.xmark', }, android: '', web: 'ban', }, }, ] }, [onSubscribeMute, onSubscribeBlock]) return ( {list.isCuratelist ? (