diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-11-12 12:45:25 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-12 12:45:25 -0800 |
commit | d9e0a927c1c98ebd6aa3885ab517af27e7de2522 (patch) | |
tree | cee196297391e497f1aa3b650d66633f3a86ca34 /src/view/screens/ProfileList.tsx | |
parent | 05b728fffcdb17708fdb52685725faf7fdc545bc (diff) | |
download | voidsky-d9e0a927c1c98ebd6aa3885ab517af27e7de2522.tar.zst |
Refactor lists to use new queries (#1875)
* Refactor lists queries to react-query * Delete old lists-list model * Implement list, list-members, and list-memberships react-queries * Update CreateOrEditList modal * First pass at my-follows and actor-autocomplete queries * Update ListAddUserModal to use new queries, change to ListAddRemoveUsersModal * Update UserAddRemoveLists modal * Remove old TODO * Fix indent, autocomplete query * Add a todo --------- Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/view/screens/ProfileList.tsx')
-rw-r--r-- | src/view/screens/ProfileList.tsx | 494 |
1 files changed, 224 insertions, 270 deletions
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 03c1703c1..18665f519 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -10,8 +10,7 @@ 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 {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' @@ -28,7 +27,6 @@ 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 {FeedDescriptor} from '#/state/queries/post-feed' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' @@ -39,17 +37,24 @@ import {OnScrollHandler} 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' -import {logger} from '#/logger' +import {ListMembers} from '#/view/com/lists/ListMembers' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSetMinimalShellMode} from '#/state/shell' import {useModalControls} from '#/state/modals' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' +import { + useListQuery, + useListMuteMutation, + useListBlockMutation, + useListDeleteMutation, +} from '#/state/queries/list' +import {cleanError} from '#/lib/strings/errors' +import {useSession} from '#/state/session' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -60,40 +65,32 @@ interface SectionRef { type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> export const ProfileListScreen = withAuthRequired( - observer(function ProfileListScreenImpl(props: Props) { - const store = useStores() - const {name: handleOrDid} = props.route.params - const [listOwnerDid, setListOwnerDid] = React.useState<string | undefined>() - const [error, setError] = React.useState<string | undefined>() - - 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]) + function ProfileListScreenImpl(props: Props) { + const {name: handleOrDid, rkey} = props.route.params + const {data: resolvedUri, error: resolveError} = useResolveUriQuery( + AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), + ) + const {data: list, error: listError} = useListQuery(resolvedUri) - if (error) { + if (resolveError) { + return ( + <CenteredView> + <ErrorScreen + error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`} + /> + </CenteredView> + ) + } + if (listError) { return ( <CenteredView> - <ErrorScreen error={error} /> + <ErrorScreen error={cleanError(listError)} /> </CenteredView> ) } - return listOwnerDid ? ( - <ProfileListScreenInner {...props} listOwnerDid={listOwnerDid} /> + return resolvedUri && list ? ( + <ProfileListScreenLoaded {...props} uri={resolvedUri} list={list} /> ) : ( <CenteredView> <View style={s.p20}> @@ -101,192 +98,172 @@ export const ProfileListScreen = withAuthRequired( </View> </CenteredView> ) - }), + }, ) -export const ProfileListScreenInner = observer( - function ProfileListScreenInnerImpl({ - route, - listOwnerDid, - }: Props & {listOwnerDid: string}) { - const store = useStores() - const {_} = useLingui() - const queryClient = useQueryClient() - const setMinimalShellMode = useSetMinimalShellMode() - const {rkey} = route.params - const listUri = `at://${listOwnerDid}/app.bsky.graph.list/${rkey}` - const feedSectionRef = React.useRef<SectionRef>(null) - const aboutSectionRef = React.useRef<SectionRef>(null) - const {openModal} = useModalControls() - - const list: ListModel = useMemo(() => { - const model = new ListModel(store, listUri) - return model - }, [store, listUri]) - useSetTitle(list.data?.name) - - useFocusEffect( - useCallback(() => { - setMinimalShellMode(false) - list.loadMore(true) - }, [setMinimalShellMode, list]), - ) - - const onPressAddUser = useCallback(() => { - openModal({ - name: 'list-add-user', - list, - onAdd() { - if (list.isCuratelist) { - queryClient.invalidateQueries({ - queryKey: FEED_RQKEY(`list|${listUri}`), - }) - } - }, - }) - }, [openModal, list, queryClient, listUri]) +function ProfileListScreenLoaded({ + route, + uri, + list, +}: Props & {uri: string; list: AppBskyGraphDefs.ListView}) { + const store = useStores() + const {_} = useLingui() + const queryClient = useQueryClient() + const setMinimalShellMode = useSetMinimalShellMode() + const {rkey} = route.params + const feedSectionRef = React.useRef<SectionRef>(null) + const aboutSectionRef = React.useRef<SectionRef>(null) + const {openModal} = useModalControls() + const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' + + useSetTitle(list.name) + + useFocusEffect( + useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const onCurrentPageSelected = React.useCallback( - (index: number) => { - if (index === 0) { - feedSectionRef.current?.scrollToTop() - } - if (index === 1) { - aboutSectionRef.current?.scrollToTop() + const onPressAddUser = useCallback(() => { + openModal({ + name: 'list-add-remove-users', + list, + onChange() { + if (isCurateList) { + queryClient.invalidateQueries({ + // TODO(eric) should construct these strings with a fn too + queryKey: FEED_RQKEY(`list|${list.uri}`), + }) } }, - [feedSectionRef], - ) + }) + }, [openModal, list, isCurateList, queryClient]) - const renderHeader = useCallback(() => { - return <Header rkey={rkey} list={list} /> - }, [rkey, list]) + const onCurrentPageSelected = React.useCallback( + (index: number) => { + if (index === 0) { + feedSectionRef.current?.scrollToTop() + } + if (index === 1) { + aboutSectionRef.current?.scrollToTop() + } + }, + [feedSectionRef], + ) - if (list.isCuratelist) { - return ( - <View style={s.hContentRegion}> - <PagerWithHeader - items={SECTION_TITLES_CURATE} - isHeaderReady={list.hasLoaded} - renderHeader={renderHeader} - onCurrentPageSelected={onCurrentPageSelected}> - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( - <FeedSection - ref={feedSectionRef} - feed={`list|${listUri}`} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - onScroll={onScroll} - headerHeight={headerHeight} - isScrolledDown={isScrolledDown} - /> - )} - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( - <AboutSection - ref={aboutSectionRef} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - 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={_(msg`New post`)} - accessibilityHint="" - /> - </View> - ) - } - if (list.isModlist) { - return ( - <View style={s.hContentRegion}> - <PagerWithHeader - items={SECTION_TITLES_MOD} - isHeaderReady={list.hasLoaded} - renderHeader={renderHeader}> - {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( - <AboutSection - list={list} - scrollElRef={ - scrollElRef as React.MutableRefObject<FlatList<any> | null> - } - 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={_(msg`New post`)} - accessibilityHint="" - /> - </View> - ) - } + const renderHeader = useCallback(() => { + return <Header rkey={rkey} list={list} /> + }, [rkey, list]) + + if (isCurateList) { return ( - <CenteredView sideBorders style={s.hContentRegion}> - <Header rkey={rkey} list={list} /> - {list.error ? <ErrorScreen error={list.error} /> : null} - </CenteredView> + <View style={s.hContentRegion}> + <PagerWithHeader + items={SECTION_TITLES_CURATE} + isHeaderReady={true} + renderHeader={renderHeader} + onCurrentPageSelected={onCurrentPageSelected}> + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + <FeedSection + ref={feedSectionRef} + feed={`list|${uri}`} + scrollElRef={ + scrollElRef as React.MutableRefObject<FlatList<any> | null> + } + onScroll={onScroll} + headerHeight={headerHeight} + isScrolledDown={isScrolledDown} + /> + )} + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + <AboutSection + ref={aboutSectionRef} + scrollElRef={ + scrollElRef as React.MutableRefObject<FlatList<any> | null> + } + list={list} + 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={_(msg`New post`)} + accessibilityHint="" + /> + </View> ) - }, -) + } + return ( + <View style={s.hContentRegion}> + <PagerWithHeader + items={SECTION_TITLES_MOD} + isHeaderReady={true} + renderHeader={renderHeader}> + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + <AboutSection + list={list} + scrollElRef={ + scrollElRef as React.MutableRefObject<FlatList<any> | null> + } + 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={_(msg`New post`)} + accessibilityHint="" + /> + </View> + ) +} -const Header = observer(function HeaderImpl({ - rkey, - list, -}: { - rkey: string - list: ListModel -}) { +function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const pal = usePalette('default') const palInverted = usePalette('inverted') const {_} = useLingui() const navigation = useNavigation<NavigationProp>() + const {currentAccount} = useSession() const {openModal, closeModal} = useModalControls() + const listMuteMutation = useListMuteMutation() + const listBlockMutation = useListBlockMutation() + const listDeleteMutation = useListDeleteMutation() + const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' + const isModList = list.purpose === 'app.bsky.graph.defs#modlist' + const isPinned = false // TODO + const isBlocking = !!list.viewer?.blocked + const isMuting = !!list.viewer?.muted + const isOwner = list.creator.did === currentAccount?.did const onTogglePinned = useCallback(async () => { Haptics.default() - list.togglePin().catch(e => { - Toast.show('There was an issue contacting the server') - logger.error('Failed to toggle pinned list', {error: e}) - }) - }, [list]) + // TODO + // list.togglePin().catch(e => { + // Toast.show('There was an issue contacting the server') + // logger.error('Failed to toggle pinned list', {error: e}) + // }) + }, []) const onSubscribeMute = useCallback(() => { openModal({ @@ -297,7 +274,7 @@ const Header = observer(function HeaderImpl({ confirmBtnText: 'Mute this List', async onPressConfirm() { try { - await list.mute() + await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) Toast.show('List muted') } catch { Toast.show( @@ -309,18 +286,18 @@ const Header = observer(function HeaderImpl({ closeModal() }, }) - }, [openModal, closeModal, list]) + }, [openModal, closeModal, list, listMuteMutation]) const onUnsubscribeMute = useCallback(async () => { try { - await list.unmute() + await listMuteMutation.mutateAsync({uri: list.uri, mute: false}) Toast.show('List unmuted') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } - }, [list]) + }, [list, listMuteMutation]) const onSubscribeBlock = useCallback(() => { openModal({ @@ -331,7 +308,7 @@ const Header = observer(function HeaderImpl({ confirmBtnText: 'Block this List', async onPressConfirm() { try { - await list.block() + await listBlockMutation.mutateAsync({uri: list.uri, block: true}) Toast.show('List blocked') } catch { Toast.show( @@ -343,26 +320,23 @@ const Header = observer(function HeaderImpl({ closeModal() }, }) - }, [openModal, closeModal, list]) + }, [openModal, closeModal, list, listBlockMutation]) const onUnsubscribeBlock = useCallback(async () => { try { - await list.unblock() + await listBlockMutation.mutateAsync({uri: list.uri, block: false}) Toast.show('List unblocked') } catch { Toast.show( 'There was an issue. Please check your internet connection and try again.', ) } - }, [list]) + }, [list, listBlockMutation]) const onPressEdit = useCallback(() => { openModal({ name: 'create-or-edit-list', list, - onSave() { - list.refresh() - }, }) }, [openModal, list]) @@ -372,7 +346,7 @@ const Header = observer(function HeaderImpl({ title: 'Delete List', message: 'Are you sure?', async onPressConfirm() { - await list.delete() + await listDeleteMutation.mutateAsync({uri: list.uri}) Toast.show('List deleted') if (navigation.canGoBack()) { navigation.goBack() @@ -381,26 +355,22 @@ const Header = observer(function HeaderImpl({ } }, }) - }, [openModal, list, navigation]) + }, [openModal, list, listDeleteMutation, navigation]) const onPressReport = useCallback(() => { - if (!list.data) return openModal({ name: 'report', uri: list.uri, - cid: list.data.cid, + cid: list.cid, }) }, [openModal, list]) const onPressShare = useCallback(() => { - const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`) + const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`) shareUrl(url) - }, [list.creatorDid, rkey]) + }, [list, rkey]) const dropdownItems: DropdownItem[] = useMemo(() => { - if (!list.hasLoaded) { - return [] - } let items: DropdownItem[] = [ { testID: 'listHeaderDropdownShareBtn', @@ -415,7 +385,7 @@ const Header = observer(function HeaderImpl({ }, }, ] - if (list.isOwner) { + if (isOwner) { items.push({label: 'separator'}) items.push({ testID: 'listHeaderDropdownEditBtn', @@ -457,14 +427,7 @@ const Header = observer(function HeaderImpl({ }) } return items - }, [ - list.hasLoaded, - list.isOwner, - onPressShare, - onPressEdit, - onPressDelete, - onPressReport, - ]) + }, [isOwner, onPressShare, onPressEdit, onPressDelete, onPressReport]) const subscribeDropdownItems: DropdownItem[] = useMemo(() => { return [ @@ -497,32 +460,28 @@ const Header = observer(function HeaderImpl({ 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} + href={makeListLink(list.creator.handle || list.creator.did || '', rkey)} + title={list.name} + avatar={list.avatar} + isOwner={list.creator.did === currentAccount?.did} + creator={list.creator} avatarType="list"> - {list.isCuratelist || list.isPinned ? ( + {isCurateList || isPinned ? ( <Button testID={list.isPinned ? 'unpinBtn' : 'pinBtn'} type={list.isPinned ? 'default' : 'inverted'} label={list.isPinned ? 'Unpin' : 'Pin to home'} onPress={onTogglePinned} /> - ) : list.isModlist ? ( - list.isBlocking ? ( + ) : isModList ? ( + isBlocking ? ( <Button testID="unblockBtn" type="default" label="Unblock" onPress={onUnsubscribeBlock} /> - ) : list.isMuting ? ( + ) : isMuting ? ( <Button testID="unmuteBtn" type="default" @@ -554,7 +513,7 @@ const Header = observer(function HeaderImpl({ </NativeDropdown> </ProfileSubpageHeader> ) -}) +} interface FeedSectionProps { feed: FeedDescriptor @@ -610,11 +569,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( ) interface AboutSectionProps { - list: ListModel - descriptionRT: RichTextAPI | null - creator: {did: string; handle: string} | undefined - isCurateList: boolean | undefined - isOwner: boolean | undefined + list: AppBskyGraphDefs.ListView onPressAddUser: () => void onScroll: OnScrollHandler headerHeight: number @@ -623,23 +578,26 @@ interface AboutSectionProps { } const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( function AboutSectionImpl( - { - list, - descriptionRT, - creator, - isCurateList, - isOwner, - onPressAddUser, - onScroll, - headerHeight, - isScrolledDown, - scrollElRef, - }, + {list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef}, ref, ) { const pal = usePalette('default') const {_} = useLingui() const {isMobile} = useWebMediaQueries() + const {currentAccount} = useSession() + const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' + const isOwner = list.creator.did === currentAccount?.did + + const descriptionRT = useMemo( + () => + list.description + ? new RichTextAPI({ + text: list.description, + facets: list.descriptionFacets, + }) + : undefined, + [list], + ) const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({offset: -headerHeight}) @@ -650,9 +608,6 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( })) const renderHeader = React.useCallback(() => { - if (!list.data) { - return <View /> - } return ( <View> <View @@ -685,8 +640,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( 'you' ) : ( <TextLink - text={sanitizeHandle(creator?.handle || '', '@')} - href={creator ? makeProfileLink(creator) : ''} + text={sanitizeHandle(list.creator.handle || '', '@')} + href={makeProfileLink(list.creator)} style={pal.textLight} /> )} @@ -728,10 +683,9 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( ) }, [ pal, - list.data, + list, isMobile, descriptionRT, - creator, isCurateList, isOwner, onPressAddUser, @@ -750,12 +704,12 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( return ( <View> - <ListItems + <ListMembers testID="listItems" + list={list.uri} scrollElRef={scrollElRef} renderHeader={renderHeader} renderEmptyState={renderEmptyState} - list={list} headerOffset={headerHeight} onScroll={onScroll} scrollEventThrottle={1} |