diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-11-01 16:15:40 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-01 16:15:40 -0700 |
commit | f57a8cf8ba0cd10a54abf35d960d8fb90266fa6b (patch) | |
tree | a9da6032bcbd587d92fd1030e698aea2dbef9f72 /src/view/screens/ProfileList.tsx | |
parent | f9944b55e26fe6109bc2e7a25b88979111470ed9 (diff) | |
download | voidsky-f57a8cf8ba0cd10a54abf35d960d8fb90266fa6b.tar.zst |
Lists updates: curate lists and blocklists (#1689)
* Add lists screen * Update Lists screen and List create/edit modal to support curate lists * Rework the ProfileList screen and add curatelist support * More ProfileList progress * Update list modals * Rename mutelists to modlists * Layout updates/fixes * More layout fixes * Modal fixes * List list screen updates * Update feed page to give more info * Layout fixes to ListAddUser modal * Layout fixes to FlatList and Feed on desktop * Layout fix to LoadLatestBtn on Web * Handle did resolution before showing the ProfileList screen * Rename the CustomFeed routes to ProfileFeed for consistency * Fix layout issues with the pager and feeds * Factor out some common code * Fix UIs for mobile * Fix user list rendering * Fix: dont bubble custom feed errors in the merge feed * Refactor feed models to reduce usage of the SavedFeeds model * Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists * Add the ability to pin lists * Add pinned lists to mobile * Remove dead code * Rework the ProfileScreenHeader to create more real-estate for action buttons * Improve layout behavior on web mobile breakpoints * Refactor feed & list pages to use new Tabs layout component * Refactor to ProfileSubpageHeader * Implement modlist block and mute * Switch to new api and just modify state on modlist actions * Fix some UI overflows * Fix: dont show edit buttons on lists you dont own * Fix alignment issue on long titles * Improve loading and error states for feeds & lists * Update list dropdown icons for ios * Fetch feed display names in the mergefeed * Improve rendering off offline feeds in the feed-listing page * Update Feeds listing UI to react to changes in saved/pinned state * Refresh list and feed on posts tab press * Fix pinned feed ordering UI * Fixes to list pinning * Remove view=simple qp * Add list to feed tuners * Render richtext * Add list href * Add 'view avatar' * Remove unused import * Fix missing import * Correctly reflect block by list state * Replace the <Tabs> component with the more effective <PagerWithHeader> component * Improve the responsiveness of the PagerWithHeader * Fix visual jank in the feed loading state * Improve performance of the PagerWithHeader * Fix a case that would cause the header to animate too aggressively * Add the ability to scroll to top by tapping the selected tab * Fix unit test runner * Update modlists test * Add curatelist tests * Fix: remove link behavior in ListAddUser modal * Fix some layout jank in the PagerWithHeader on iOS * Simplify ListItems header rendering * Wait for the appview to recognize the list before proceeding with list creation * Fix glitch in the onPageSelecting index of the Pager * Fix until() * Copy fix Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/view/screens/ProfileList.tsx')
-rw-r--r-- | src/view/screens/ProfileList.tsx | 850 |
1 files changed, 743 insertions, 107 deletions
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 11a847db3..859f50bef 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -1,166 +1,802 @@ -import React from 'react' -import {StyleSheet} from 'react-native' +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 {ViewHeader} from 'view/com/util/ViewHeader' +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 {ListItems} from 'view/com/lists/ListItems' 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 {ListActions} from 'view/com/lists/ListActions' +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<CommonNavigatorParams, 'ProfileList'> export const ProfileListScreen = withAuthRequired( - observer(function ProfileListScreenImpl({route}: Props) { + observer(function ProfileListScreenImpl(props: Props) { + const pal = usePalette('default') const store = useStores() const navigation = useNavigation<NavigationProp>() - const {isTabletOrDesktop} = useWebMediaQueries() - const pal = usePalette('default') - const {name, rkey} = route.params - const list: ListModel = React.useMemo(() => { + const {name: handleOrDid} = props.route.params + + const [listOwnerDid, setListOwnerDid] = React.useState<string | undefined>() + const [error, setError] = React.useState<string | undefined>() + + 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 ( + <CenteredView> + <View + style={[ + pal.view, + pal.border, + { + margin: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 6, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.mb10]}> + Could not load list + </Text> + <Text type="md" style={[pal.text, s.mb20]}> + {error} + </Text> + + <View style={{flexDirection: 'row'}}> + <Button + type="default" + accessibilityLabel="Go Back" + accessibilityHint="Return to previous page" + onPress={onPressBack} + style={{flexShrink: 1}}> + <Text type="button" style={pal.text}> + Go Back + </Text> + </Button> + </View> + </View> + </CenteredView> + ) + } + + return listOwnerDid ? ( + <ProfileListScreenInner {...props} listOwnerDid={listOwnerDid} /> + ) : ( + <CenteredView> + <View style={s.p20}> + <ActivityIndicator size="large" /> + </View> + </CenteredView> + ) + }), +) + +export const ProfileListScreenInner = observer( + function ProfileListScreenInnerImpl({ + route, + listOwnerDid, + }: Props & {listOwnerDid: string}) { + const store = useStores() + const {rkey} = route.params + const feedSectionRef = React.useRef<SectionRef>(null) + const aboutSectionRef = React.useRef<SectionRef>(null) + + const list: ListModel = useMemo(() => { const model = new ListModel( store, - `at://${name}/app.bsky.graph.list/${rkey}`, + `at://${listOwnerDid}/app.bsky.graph.list/${rkey}`, ) return model - }, [store, name, rkey]) - useSetTitle(list.list?.name) + }, [store, listOwnerDid, rkey]) + const feed = useMemo( + () => new PostsFeedModel(store, 'list', {list: list.uri}), + [store, list], + ) + useSetTitle(list.data?.name) useFocusEffect( - React.useCallback(() => { + useCallback(() => { store.shell.setMinimalShellMode(false) - list.loadMore(true) - }, [store, list]), + list.loadMore(true).then(() => { + if (list.isCuratelist) { + feed.setup() + } + }) + }, [store, list, feed]), ) - const onToggleSubscribed = React.useCallback(async () => { - try { - if (list.list?.viewer?.muted) { - await list.unsubscribe() - } else { - await list.subscribe() - } - } catch (err) { - Toast.show( - 'There was an an issue updating your subscription, please check your internet connection and try again.', - ) - store.log.error('Failed up update subscription', {err}) - } - }, [store, list]) - - const onPressEditList = React.useCallback(() => { + const onPressAddUser = useCallback(() => { store.shell.openModal({ - name: 'create-or-edit-mute-list', + name: 'list-add-user', list, - onSave() { - list.refresh() + onAdd() { + if (list.isCuratelist) { + feed.refresh() + } }, }) - }, [store, list]) + }, [store, list, feed]) - const onPressDeleteList = React.useCallback(() => { - store.shell.openModal({ - name: 'confirm', - title: 'Delete List', - message: 'Are you sure?', - async onPressConfirm() { - await list.delete() - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } + const onCurrentPageSelected = React.useCallback( + (index: number) => { + if (index === 0) { + feedSectionRef.current?.scrollToTop() + } + if (index === 1) { + aboutSectionRef.current?.scrollToTop() + } + }, + [feedSectionRef], + ) + + const renderHeader = useCallback(() => { + return <Header rkey={rkey} list={list} /> + }, [rkey, list]) + + if (list.isCuratelist) { + return ( + <View style={s.hContentRegion}> + <PagerWithHeader + items={SECTION_TITLES_CURATE} + renderHeader={renderHeader} + onCurrentPageSelected={onCurrentPageSelected}> + {({onScroll, headerHeight, isScrolledDown}) => ( + <FeedSection + key="1" + ref={feedSectionRef} + feed={feed} + onScroll={onScroll} + headerHeight={headerHeight} + isScrolledDown={isScrolledDown} + /> + )} + {({onScroll, headerHeight, isScrolledDown}) => ( + <AboutSection + key="2" + ref={aboutSectionRef} + list={list} + descriptionRT={list.descriptionRT} + creator={list.data ? list.data.creator : undefined} + isCurateList={list.isCuratelist} + isOwner={list.isOwner} + onPressAddUser={onPressAddUser} + onScroll={onScroll} + headerHeight={headerHeight} + isScrolledDown={isScrolledDown} + /> + )} + </PagerWithHeader> + <FAB + testID="composeFAB" + onPress={() => store.shell.openComposer({})} + icon={ + <ComposeIcon2 + strokeWidth={1.5} + size={29} + style={{color: 'white'}} + /> + } + accessibilityRole="button" + accessibilityLabel="New post" + accessibilityHint="" + /> + </View> + ) + } + if (list.isModlist) { + return ( + <View style={s.hContentRegion}> + <PagerWithHeader + items={SECTION_TITLES_MOD} + renderHeader={renderHeader}> + {({onScroll, headerHeight, isScrolledDown}) => ( + <AboutSection + key="2" + list={list} + descriptionRT={list.descriptionRT} + creator={list.data ? list.data.creator : undefined} + isCurateList={list.isCuratelist} + isOwner={list.isOwner} + onPressAddUser={onPressAddUser} + onScroll={onScroll} + headerHeight={headerHeight} + isScrolledDown={isScrolledDown} + /> + )} + </PagerWithHeader> + <FAB + testID="composeFAB" + onPress={() => store.shell.openComposer({})} + icon={ + <ComposeIcon2 + strokeWidth={1.5} + size={29} + style={{color: 'white'}} + /> + } + accessibilityRole="button" + accessibilityLabel="New post" + accessibilityHint="" + /> + </View> + ) + } + return <Header rkey={rkey} list={list} /> + }, +) + +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<NavigationProp>() + + 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', }, }) - }, [store, list, navigation]) - - const onPressReportList = React.useCallback(() => { - if (!list.list) return - store.shell.openModal({ - name: 'report', - uri: list.uri, - cid: list.list.cid, + items.push({ + testID: 'listHeaderDropdownDeleteBtn', + label: 'Delete List', + onPress: onPressDelete, + icon: { + ios: { + name: 'trash', + }, + android: '', + web: ['far', 'trash-can'], + }, }) - }, [store, list]) + } 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 ( + <ProfileSubpageHeader + isLoading={!list.hasLoaded} + href={makeListLink( + list.data?.creator.handle || list.data?.creator.did || '', + rkey, + )} + title={list.data?.name || 'User list'} + avatar={list.data?.avatar} + isOwner={list.isOwner} + creator={list.data?.creator} + avatarType="list"> + {list.isCuratelist ? ( + <Button + testID={list.isPinned ? 'unpinBtn' : 'pinBtn'} + type={list.isPinned ? 'default' : 'inverted'} + label={list.isPinned ? 'Unpin' : 'Pin to home'} + onPress={onTogglePinned} + /> + ) : list.isModlist ? ( + list.isBlocking ? ( + <Button + testID="unblockBtn" + type="default" + label="Unblock" + onPress={onUnsubscribeBlock} + /> + ) : list.isMuting ? ( + <Button + testID="unmuteBtn" + type="default" + label="Unmute" + onPress={onUnsubscribeMute} + /> + ) : ( + <NativeDropdown + testID="subscribeBtn" + items={subscribeDropdownItems} + accessibilityLabel="Subscribe to this list" + accessibilityHint=""> + <View style={[palInverted.view, styles.btn]}> + <Text style={palInverted.text}>Subscribe</Text> + </View> + </NativeDropdown> + ) + ) : null} + <NativeDropdown + testID="headerDropdownBtn" + items={dropdownItems} + accessibilityLabel="More options" + accessibilityHint=""> + <View style={[pal.viewLight, styles.btn]}> + <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} /> + </View> + </NativeDropdown> + </ProfileSubpageHeader> + ) +}) + +interface FeedSectionProps { + feed: PostsFeedModel + onScroll: OnScrollCb + headerHeight: number + isScrolledDown: boolean +} +const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( + function FeedSectionImpl( + {feed, onScroll, headerHeight, isScrolledDown}, + ref, + ) { + const hasNew = feed.hasNewLatest && !feed.isRefreshing + const scrollElRef = React.useRef<FlatList>(null) - const onPressShareList = React.useCallback(() => { - const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`) - shareUrl(url) - }, [list.creatorDid, rkey]) + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: -headerHeight}) + }, [scrollElRef, headerHeight]) - const renderEmptyState = React.useCallback(() => { - return <EmptyState icon="users-slash" message="This list is empty!" /> + const onPressLoadLatest = React.useCallback(() => { + onScrollToTop() + feed.refresh() + }, [feed, onScrollToTop]) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const renderPostsEmpty = useCallback(() => { + return <EmptyState icon="feed" message="This feed is empty!" /> }, []) - const renderHeaderBtns = React.useCallback(() => { - return ( - <ListActions - muted={list.list?.viewer?.muted} - isOwner={list.isOwner} - onPressDeleteList={onPressDeleteList} - onPressEditList={onPressEditList} - onToggleSubscribed={onToggleSubscribed} - onPressShareList={onPressShareList} - onPressReportList={onPressReportList} - reversed={true} + return ( + <View> + <Feed + testID="listFeed" + feed={feed} + scrollElRef={scrollElRef} + onScroll={onScroll} + scrollEventThrottle={1} + renderEmptyState={renderPostsEmpty} + headerOffset={headerHeight} /> + {(isScrolledDown || hasNew) && ( + <LoadLatestBtn + onPress={onPressLoadLatest} + label="Load new posts" + showIndicator={hasNew} + /> + )} + </View> + ) + }, +) + +interface AboutSectionProps { + list: ListModel + descriptionRT: RichTextAPI | null + creator: {did: string; handle: string} | undefined + isCurateList: boolean | undefined + isOwner: boolean | undefined + onPressAddUser: () => void + onScroll: OnScrollCb + headerHeight: number + isScrolledDown: boolean +} +const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( + function AboutSectionImpl( + { + list, + descriptionRT, + creator, + isCurateList, + isOwner, + onPressAddUser, + onScroll, + headerHeight, + isScrolledDown, + }, + ref, + ) { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + const scrollElRef = React.useRef<FlatList>(null) + + const onScrollToTop = useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: -headerHeight}) + }, [scrollElRef, headerHeight]) + + React.useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const renderHeader = React.useCallback(() => { + if (!list.data) { + return <View /> + } + return ( + <View> + <View + style={[ + { + borderTopWidth: 1, + padding: isMobile ? 14 : 20, + gap: 12, + }, + pal.border, + ]}> + {descriptionRT ? ( + <RichText + testID="listDescription" + type="lg" + style={pal.text} + richText={descriptionRT} + /> + ) : ( + <Text + testID="listDescriptionEmpty" + type="lg" + style={[{fontStyle: 'italic'}, pal.textLight]}> + No description + </Text> + )} + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + {isCurateList ? 'User list' : 'Moderation list'} by{' '} + {isOwner ? ( + 'you' + ) : ( + <TextLink + text={sanitizeHandle(creator?.handle || '', '@')} + href={creator ? makeProfileLink(creator) : ''} + style={pal.textLight} + /> + )} + </Text> + </View> + <View + style={[ + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: isMobile ? 14 : 20, + paddingBottom: isMobile ? 14 : 18, + }, + ]}> + <Text type="lg-bold">Users</Text> + {isOwner && ( + <Pressable + testID="addUserBtn" + accessibilityRole="button" + accessibilityLabel="Add a user to this list" + accessibilityHint="" + onPress={onPressAddUser} + style={{flexDirection: 'row', alignItems: 'center', gap: 6}}> + <FontAwesomeIcon + icon="user-plus" + color={pal.colors.link} + size={16} + /> + <Text style={pal.link}>Add</Text> + </Pressable> + )} + </View> + </View> ) }, [ - list.isOwner, - list.list?.viewer?.muted, - onPressDeleteList, - onPressEditList, - onPressShareList, - onToggleSubscribed, - onPressReportList, + pal, + list.data, + isMobile, + descriptionRT, + creator, + isCurateList, + isOwner, + onPressAddUser, ]) + const renderEmptyState = useCallback(() => { + return ( + <EmptyState + icon="users-slash" + message="This list is empty!" + style={{paddingTop: 40}} + /> + ) + }, []) + return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="moderationMutelistsScreen"> - <ViewHeader title="" renderButton={renderHeaderBtns} /> + <View> <ListItems - list={list} + testID="listItems" + scrollElRef={scrollElRef} + renderHeader={renderHeader} renderEmptyState={renderEmptyState} - onToggleSubscribed={onToggleSubscribed} - onPressEditList={onPressEditList} - onPressDeleteList={onPressDeleteList} - onPressReportList={onPressReportList} - onPressShareList={onPressShareList} - style={[s.flex1]} + list={list} + headerOffset={headerHeight} + onScroll={onScroll} + scrollEventThrottle={1} /> - </CenteredView> + {isScrolledDown && ( + <LoadLatestBtn + onPress={onScrollToTop} + label="Scroll to top" + showIndicator={false} + /> + )} + </View> ) - }), + }, ) const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 0, + btn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingVertical: 7, + paddingHorizontal: 14, + borderRadius: 50, + marginLeft: 6, }, }) |